Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions book/src/contract-keywords/property-constraints.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ An integer expression is one of:
| `divide` | `{ "divide": [a, b] }` | The Euclidean quotient of `a` by `b` |
| `modulo` | `{ "modulo": [a, b] }` | The Euclidean remainder of `a` by `b`, never negative |
| `power` | `{ "power": [a, b] }` | `a` to the power `b` |
| `length`, `byteLength` | `{ "length": "title" }` | The characters (as `maxLength` counts them) or UTF-8 bytes (as `maxBytes` counts them) of a string property, 0 when the document leaves it out |
| `count` | `{ "count": "tags" }` | The items of an array property, or the bytes of a byte array property, 0 when the document leaves it out |

Where `maxLength`, `maxBytes` and `maxItems` bound one property by a fixed number, a size can be compared with another property or bounded only under a condition: `{ "lessThanOrEqual": [{ "count": "tags" }, "maxTags"] }` holds a list to its own limit. A size never breaks a rule by itself: a property left out or null has size 0, and so would a value of another type, which the schema validation refuses first.

Two more forms appear only in string and identifier comparisons, never inside arithmetic:

Expand Down Expand Up @@ -162,7 +166,7 @@ The meta-schema checks the shape (`JsonSchemaError`, 10101):

The parser then checks the rules against the document type (`InvalidContractStructure`, 10231):

- every path an integer expression reads names an integer or boolean property; every path compared with a string names a string property; every path compared with an identifier names an identifier property; every path `present` or `absent` tests names a property of any type, an object included;
- every path an integer expression reads names an integer or boolean property; every path `length` or `byteLength` measures names a string property, and every path `count` counts an array or byte array property; every path compared with a string names a string property; every path compared with an identifier names an identifier property; every path `present` or `absent` tests names a property of any type, an object included;
- no rule reads a property that is `transient` or inside a transient object, since a stored document could never be held to it;
- every comparison and `in` reads at least one property: a comparison of constants would hold for every document or for none;
- strings and identifiers are compared only with `equal`, `notEqual` and `in`; a string is never compared with an identifier; a property is never compared with itself;
Expand All @@ -188,7 +192,7 @@ A rule within 32 nodes is never deep enough to reach the 64-level bound. Nodes a
| `present`, `absent` | 1 |
| `anyOf`, `allOf` | 1, plus their conditions |
| `not` | 1, plus its condition |
| An integer, a path or an `ifAbsent` | 1 |
| An integer, a path, an `ifAbsent` or a size (`length`, `byteLength`, `count`) | 1 |
| `add`, `multiply`, `subtract`, `divide`, `modulo`, `power` | 1, plus their operands |

`depositCoversOrder` above is 7 nodes (the comparison, `multiply`, `add` and four paths), and `closedNeedsClosedAt` is 5. An `in` fits up to 30 values in 32 nodes.
Expand Down
7 changes: 4 additions & 3 deletions book/src/data-model/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,19 +724,20 @@ Conditions nest: `{ "not": { "allOf": [{ "equal": ["price", 0] }, { "greaterThan
- a string, the dotted path of an integer or boolean property of the document type (`"price"`, `"meta.total"`, `"waiveFee"`), whose value it takes, 0 when the document leaves the property out. A boolean reads as 1 for true and 0 for false, so `{ "equal": [{ "multiply": ["waiveFee", "fee"] }, 0] }` says a waived fee is 0;
- `{ "ifAbsent": [path, value] }`, the property's value, or `value` when the document leaves it out (an integer value here; a string value gives a string property a default in a string comparison instead);
- `{ "add": [...] }` or `{ "multiply": [...] }` over two or more operands;
- `{ "subtract": [a, b] }`, `{ "divide": [a, b] }`, `{ "modulo": [a, b] }` or `{ "power": [a, b] }`.
- `{ "subtract": [a, b] }`, `{ "divide": [a, b] }`, `{ "modulo": [a, b] }` or `{ "power": [a, b] }`;
- a size: `{ "length": path }`, the characters of a string property (counted as `maxLength` counts them), `{ "byteLength": path }`, its UTF-8 bytes (as `maxBytes` counts them), or `{ "count": path }`, the items of an array property or the bytes of a byte array property. Where `maxLength`, `maxBytes` and `maxItems` bound one property by a fixed number, a size can be compared with another property or bounded only under a condition: `{ "lessThanOrEqual": [{ "count": "tags" }, "maxTags"] }` holds a list to its own limit, and `{ "anyOf": [{ "greaterThan": ["fee", 0] }, { "lessThanOrEqual": [{ "length": "title" }, 20] }] }` keeps a free listing's title short. A property the document leaves out or sets to null has size 0, and a size never breaks a rule by itself (a value of another type would read as 0 too, but the schema validation refuses it first).

A JSON number is always a value and a string always a path, so a property named `100` is not confused with the number, and the rule is a tree the meta-schema can check rather than a string with precedence rules to parse. Consensus holds nothing but this tree; an SDK may offer an infix spelling that compiles to it.

The arithmetic is exact over `i128`. Operands are evaluated left to right, and every intermediate result must fit: an overflow, a divisor of 0, a negative exponent or a property value that is not an integer (a float with no fractional part passes the schema's `integer` type) breaks the rule instead of wrapping or truncating. `divide` and `modulo` are Euclidean, so the remainder is never negative and the quotient is the one that goes with it (`-7` by `2` is `-4` remainder `1`); for operands that are not negative this is ordinary integer division. `0` to the power `0` is `1`. There are no floats: a `number` property cannot be read, which keeps every node's result bit-identical.

Conditions are checked in declared order and no further than the outcome needs: a comparison evaluates its left side, then its right; `anyOf` stops at the first condition that holds and `allOf` at the first that fails. A fault in a condition that is checked breaks the rule whatever the others would say, and `not` does not turn it into a pass. So an earlier condition guards a later one: `{ "anyOf": [{ "equal": ["b", 0] }, { "equal": [{ "divide": ["a", "b"] }, 2] }] }` holds for a `b` of 0 without dividing by it, while the same two conditions the other way round divide by zero and break the rule.

The parser (generation 3, meta-schema v3) checks the keyword on every parse, stored contracts included: the shape, that every path an operand reads names an integer or boolean property of the type, every path compared with a string names a string property (and every constant compared with one that declares an `enum` is one of its values) (a nested one by its dotted path) and every path `present` or `absent` tests names a property of the type, and that neither is `transient` nor inside a transient object (a transient value is never stored, so a stored document could not be held to the rule), that every comparison and `in` reads at least one property (a constant one would make its rule, or an `anyOf` around it, hold for every document or for none), that no `in` lists a value twice, that no `anyOf` or `allOf` holds one of its own kind directly and no `not` a `not`, that no literal divisor is 0 and no literal exponent negative, and that no condition or operand nests deeper than `MAX_PROPERTY_CONSTRAINT_PARSE_DEPTH` (64), a constant that keeps a parse without full validation from recursing without bound and that no registrable rule comes near. Under full validation, when a contract registers or updates, it also holds the limits: at most `SystemLimits::max_property_constraints` rules per type (16) and `max_property_constraint_nodes` nodes per rule (32), counting every comparison and logical operator, every `in` and each value it lists, every `const`, every `present` or `absent`, every arithmetic operator and every operand, and that no `anyOf` or `allOf` lists the same condition twice (conditions that parse alike, so `1` and `1.0` are the same value). The rules are fixed when the document type is created: adding, removing or changing one is an incompatible schema change (`IncompatibleDocumentTypeSchemaError`, 10246), since stored documents were judged against the rules as they were.
The parser (generation 3, meta-schema v3) checks the keyword on every parse, stored contracts included: the shape, that every path an operand reads names an integer or boolean property of the type, every path `length` or `byteLength` measures a string property, every path `count` counts an array or byte array property, every path compared with a string names a string property (and every constant compared with one that declares an `enum` is one of its values) (a nested one by its dotted path) and every path `present` or `absent` tests names a property of the type, and that neither is `transient` nor inside a transient object (a transient value is never stored, so a stored document could not be held to the rule), that every comparison and `in` reads at least one property (a constant one would make its rule, or an `anyOf` around it, hold for every document or for none), that no `in` lists a value twice, that no `anyOf` or `allOf` holds one of its own kind directly and no `not` a `not`, that no literal divisor is 0 and no literal exponent negative, and that no condition or operand nests deeper than `MAX_PROPERTY_CONSTRAINT_PARSE_DEPTH` (64), a constant that keeps a parse without full validation from recursing without bound and that no registrable rule comes near. Under full validation, when a contract registers or updates, it also holds the limits: at most `SystemLimits::max_property_constraints` rules per type (16) and `max_property_constraint_nodes` nodes per rule (32), counting every comparison and logical operator, every `in` and each value it lists, every `const`, every `present` or `absent`, every arithmetic operator and every operand (a size is one), and that no `anyOf` or `allOf` lists the same condition twice (conditions that parse alike, so `1` and `1.0` are the same value). The rules are fixed when the document type is created: adding, removing or changing one is an incompatible schema change (`IncompatibleDocumentTypeSchemaError`, 10246), since stored documents were judged against the rules as they were.

Enforcement lives in `DataContract::validate_document_properties` (generation 0, extended in place: the call is inert before protocol version 14, where `validate_property_constraints` is `None`), after the schema validation. Document create and replace structure validation call it, so consensus applies the rules, and so does every client that validates a document before sending it. The rules are checked in name order against the document's properties, which for a replace is the whole document, and the first one broken fails with `DocumentPropertyConstraintViolatedError` (basic code 10422), naming the document type, the rule and why: the rule does not hold, or evaluating it overflowed, divided by zero, raised to a negative power or read a value that is not an integer. The check reads no state and changes nothing stored, so it adds no fee; the limits bound its cost. `$ownerId` reads the writer, passed in by create and replace (`validate_document_properties` takes the owner; a client that does not know it passes `None`, and `$ownerId` then equals no identifier). Transfers and purchases change no property but the owner, so only the rules reading `$ownerId` are judged again, with the new owner (`DocumentTypeV0Methods::validate_property_constraints_for_new_owner`, next to the `distinctFrom` check); price updates change neither and are not judged.

In Rust the rules are `DocumentTypeV2Getters::property_constraints` (a map from name to `PropertyConstraint`: a comparison, an `in`, a string comparison (`TextCompare`, `TextIn`), a `present` or `absent`, or an `anyOf`, `allOf` or `not` of them, empty on types that predate the keyword; `property_reads` lists what a rule reads and whether by value or by presence), each rule's `holds` and `violation` evaluate it against a document's data, and the document check is `DocumentTypeV0Methods::validate_property_constraints`.
In Rust the rules are `DocumentTypeV2Getters::property_constraints` (a map from name to `PropertyConstraint`: a comparison, an `in`, a string comparison (`TextCompare`, `TextIn`), a `present` or `absent`, or an `anyOf`, `allOf` or `not` of them, empty on types that predate the keyword; `property_reads` lists what a rule reads and how: by value, by presence, by size (`Length`, `Count`) or in a comparison of strings or identifiers), each rule's `holds` and `violation` evaluate it against a document's data, and the document check is `DocumentTypeV0Methods::validate_property_constraints`.

## Rules and Guidelines

Expand Down
2 changes: 1 addition & 1 deletion packages/js-evo-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ From protocol version 14 a document type can declare rules its documents' proper
}
```

The comparisons are `equal`, `notEqual`, `lessThan`, `lessThanOrEqual`, `greaterThan` and `greaterThanOrEqual`, and the operators `add` and `multiply` (two or more operands) and `subtract`, `divide`, `modulo` and `power` (exactly two). `{ "in": [expression, [values]] }` holds if the expression takes one of two or more distinct integer values. A string property (an enum, say) is compared with `{ "equal": ["status", { "const": "closed" }] }` or `notEqual`, with another string property (`{ "notEqual": ["fromCurrency", "toCurrency"] }`), or listed with `{ "in": ["status", ["open", "pending"]] }`; a constant must be one of the property's `enum` values, and a string the document leaves out equals none, unless `{ "ifAbsent": ["status", "open"] }` gives it a default. Identifier properties compare the same way, with base58 constants: `{ "equal": ["paymentToken", { "const": "<base58>" }] }`, `{ "notEqual": ["buyerId", "sellerId"] }`, or `{ "in": ["paymentToken", ["<base58>", "<base58>"]] }`. `$ownerId`, the document's owner, is an identifier operand as well (`{ "equal": ["authorId", "$ownerId"] }`), and a transfer or purchase that would break such a rule is refused. `anyOf` holds if at least one of two or more conditions holds, `allOf` if every one does, and `not` if its one condition does not; conditions are checked in order and `anyOf` stops at the first that holds, so `{ "anyOf": [{ "equal": ["b", 0] }, { "equal": [{ "divide": ["a", "b"] }, 2] }] }` never divides by zero. An operand may read an integer or a boolean property (true as 1, false as 0). A property the document leaves out counts as 0, or as the value of an `ifAbsent` operand naming it; `{ "present": path }` and `{ "absent": path }` tell a property left out from one set to 0, and may name a property of any type. The arithmetic is exact over 128-bit integers, and `divide` and `modulo` are Euclidean, so a remainder is never negative. The rules are fixed when the document type is created.
The comparisons are `equal`, `notEqual`, `lessThan`, `lessThanOrEqual`, `greaterThan` and `greaterThanOrEqual`, and the operators `add` and `multiply` (two or more operands) and `subtract`, `divide`, `modulo` and `power` (exactly two). `{ "in": [expression, [values]] }` holds if the expression takes one of two or more distinct integer values. A string property (an enum, say) is compared with `{ "equal": ["status", { "const": "closed" }] }` or `notEqual`, with another string property (`{ "notEqual": ["fromCurrency", "toCurrency"] }`), or listed with `{ "in": ["status", ["open", "pending"]] }`; a constant must be one of the property's `enum` values, and a string the document leaves out equals none, unless `{ "ifAbsent": ["status", "open"] }` gives it a default. Identifier properties compare the same way, with base58 constants: `{ "equal": ["paymentToken", { "const": "<base58>" }] }`, `{ "notEqual": ["buyerId", "sellerId"] }`, or `{ "in": ["paymentToken", ["<base58>", "<base58>"]] }`. `$ownerId`, the document's owner, is an identifier operand as well (`{ "equal": ["authorId", "$ownerId"] }`), and a transfer or purchase that would break such a rule is refused. `anyOf` holds if at least one of two or more conditions holds, `allOf` if every one does, and `not` if its one condition does not; conditions are checked in order and `anyOf` stops at the first that holds, so `{ "anyOf": [{ "equal": ["b", 0] }, { "equal": [{ "divide": ["a", "b"] }, 2] }] }` never divides by zero. An operand may read an integer or a boolean property (true as 1, false as 0), or a size: `{ "length": path }` and `{ "byteLength": path }` give the characters and UTF-8 bytes of a string property, and `{ "count": path }` the items of an array or the bytes of a byte array, so `{ "lessThanOrEqual": [{ "count": "tags" }, "maxTags"] }` holds a list to its own limit (a size read is reported with kind `length` or `count`). A property the document leaves out counts as 0, or as the value of an `ifAbsent` operand naming it; `{ "present": path }` and `{ "absent": path }` tell a property left out from one set to 0, and may name a property of any type. The arithmetic is exact over 128-bit integers, and `divide` and `modulo` are Euclidean, so a remainder is never negative. The rules are fixed when the document type is created.

Consensus checks every rule on each create and replace, and rejects a document that breaks one, or whose rule overflows, divides by zero or raises to a negative power. The code reaches JS as `error.code`, and the message names the rule:

Expand Down
Loading
Loading