Skip to content

Commit 81ee8df

Browse files
feat(platform)!: creation, update and transfer times and heights in propertyConstraints rules (PV14) (#5078)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 10b1c2c commit 81ee8df

28 files changed

Lines changed: 1670 additions & 268 deletions

File tree

‎book/src/contract-keywords/property-constraints.md‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@
4949

5050
- **Create and replace.** The rules run after the JSON schema validation of the document's properties (and after [maxBytes](max-bytes.md)), so every value a rule reads has passed its property's schema. A replace is judged on the whole new document, not only on what changed.
5151
- **Name order, first failure.** Rules are checked in the order of their names, and the first rule the document breaks refuses the transition with `DocumentPropertyConstraintViolatedError` (10422). The error names the document type, the rule, and why it failed (below).
52-
- **Transfer and purchase.** These change only the owner. Rules that read `$ownerId` are judged again, against the stored document and its new owner; other rules are not, since nothing they read changed. A transfer or purchase that would break an owner rule is refused with 10422.
53-
- **Price updates and deletes** are not judged, with one exception: a delete of an [index-only](index-only.md) document carries the row's values, which are validated like a create's, rules included. The delete does not carry the owner, which is why an index-only type may not have a rule reading `$ownerId`.
54-
- **No state, no fee.** A rule reads only the document and its owner. It changes nothing stored and adds no fee; the limits below bound its cost. SDKs that validate a document before sending it apply the same rules.
52+
- **Transfer and purchase.** These change only the owner and the transfer's time and heights. Rules that read `$ownerId` or `$transferredAt…` are judged again, against the stored document with its new owner and transfer values; other rules are not, since nothing they read changed. A transfer or purchase that would break such a rule is refused with 10422.
53+
- **Price updates** change only the update's time and heights, so the rules that read `$updatedAt…` are judged again the same way; other rules are not.
54+
- **Deletes** are not judged, with one exception: a delete of an [index-only](index-only.md) document carries the row's values, which are validated like a create's, rules included. The delete carries neither the owner nor any time or height, which is why an index-only type may not have a rule reading `$ownerId` or a system time or height.
55+
- **No state, no fee.** A rule reads only the document, its owner and its times and heights. It changes nothing stored and adds no fee; the limits below bound its cost. SDKs that validate a document before sending it apply the same rules.
5556

5657
Why a rule fails, as the error reports it:
5758

@@ -98,6 +99,7 @@ An integer expression is one of:
9899
| `power` | `{ "power": [a, b] }` | `a` to the power `b` |
99100
| `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 |
100101
| `count` | `{ "count": "tags" }` | The items of an array property, or the bytes of a byte array property, 0 when the document leaves it out |
102+
| system time or height | `"$createdAt"`, `"$updatedAtBlockHeight"` | A time or height the document records (see [Times and heights](#times-and-heights)) |
101103

102104
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.
103105

@@ -108,7 +110,7 @@ Two more forms appear only in string and identifier comparisons, never inside ar
108110
| `{ "const": "closed" }` | A string constant, or, compared with an identifier property or `$ownerId`, a base58 identifier |
109111
| `{ "ifAbsent": ["status", "open"] }` | A string property, read as the given string when the document leaves it out |
110112

111-
A bare JSON string is always a path and a bare JSON number always a value, so a constant string needs `{ "const": ... }`. The values an `in` lists are literals and need no wrapper. A path is a property name, or names joined by dots for a nested property (`"rewardSplit.leader"`); the only `$` name a rule accepts is `$ownerId`.
113+
A bare JSON string is always a path and a bare JSON number always a value, so a constant string needs `{ "const": ... }`. The values an `in` lists are literals and need no wrapper. A path is a property name, or names joined by dots for a nested property (`"rewardSplit.leader"`); the only `$` names a rule accepts are `$ownerId` and the times and heights below.
112114

113115
A `number` property (a float) cannot be read by a rule, which keeps every result exact.
114116

@@ -135,6 +137,24 @@ An identifier property compares in the same three ways: `{ "equal": ["paymentTok
135137

136138
It is not a property: `present`, `absent` and integer expressions refuse it, and comparing it with itself is refused. On create and replace it is the writer. A transfer or purchase is judged with the new owner, as described in [How it works](#how-it-works). An [index-only](index-only.md) type may not declare a rule that reads it.
137139

140+
## Times and heights
141+
142+
A rule can read when the document was created, last updated and last transferred, as an integer:
143+
144+
| | block time (ms) | Platform block height | Core block height |
145+
|---|---|---|---|
146+
| creation | `$createdAt` | `$createdAtBlockHeight` | `$createdAtCoreBlockHeight` |
147+
| last update: a create, a replace or a price update | `$updatedAt` | `$updatedAtBlockHeight` | `$updatedAtCoreBlockHeight` |
148+
| last transfer: a create, a transfer or a purchase | `$transferredAt` | `$transferredAtBlockHeight` | `$transferredAtCoreBlockHeight` |
149+
150+
- `{ "lessThanOrEqual": [{ "subtract": ["endsAt", "$createdAt"] }, 604800000] }` keeps a listing to a week from its creation.
151+
- `{ "lessThanOrEqual": ["$updatedAt", "endsAt"] }` refuses a replace or a price update after the listing ends.
152+
- `{ "lessThanOrEqual": ["$transferredAt", "endsAt"] }` refuses a transfer or a purchase after it ends.
153+
154+
A rule may read one only when the document type records it by listing it in `required`, so every stored document holds it. None takes an `ifAbsent` default, `present` and `absent` refuse them, and an [index-only](index-only.md) type reads none. Each write is judged with the values the stored document ends up with: a create with its block's time and heights for all three events; a replace with the stored creation and transfer values and its block's as the update; a price update with its block's as the update; a transfer or a purchase with its block's as the transfer.
155+
156+
SDK pre-checks run before the block exists: they use the device clock for the times a write records, and do not judge a rule reading a block height, which is unknown until the block.
157+
138158
## Evaluation order and short-circuiting
139159

140160
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, `allOf` at the first that fails. Operands are evaluated left to right.
@@ -162,7 +182,7 @@ The meta-schema checks the shape (`JsonSchemaError`, 10101):
162182
- every condition and every operator object has exactly one key;
163183
- a comparison, `subtract`, `divide`, `modulo` and `power` take exactly two operands; `add` and `multiply` two or more; `anyOf` and `allOf` two or more conditions, no two alike; an `in` two or more distinct values, all integers or all strings;
164184
- no `anyOf` or `allOf` holds its own kind directly, and no `not` holds a `not`;
165-
- a path matches `$ownerId` or dotted names of 1 to 64 letters, digits or underscores, so `$createdAt` and other system properties are refused.
185+
- a path matches `$ownerId`, one of the nine [times and heights](#times-and-heights), or dotted names of 1 to 64 letters, digits or underscores, so `$revision` and other system properties are refused.
166186

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

@@ -172,7 +192,8 @@ The parser then checks the rules against the document type (`InvalidContractStru
172192
- 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;
173193
- string constants and `ifAbsent` defaults are in the property's `enum` when it has one; identifier constants are base58 identifiers of 32 bytes;
174194
- no literal divisor is 0 and no literal exponent is negative;
175-
- `present` and `absent` do not name `$ownerId`, and an index-only type has no rule reading it;
195+
- every time or height a rule reads is one the type lists in `required`, and takes no `ifAbsent` default;
196+
- `present` and `absent` do not name `$ownerId` or a time or height, and an index-only type has no rule reading any of them;
176197
- no `anyOf` or `allOf` lists two conditions that parse alike, such as `1` and `1.0`, or two `in` conditions listing the same values in another order;
177198
- no condition or operand nests more than 64 levels deep.
178199

@@ -192,7 +213,7 @@ A rule within 32 nodes is never deep enough to reach the 64-level bound. Nodes a
192213
| `present`, `absent` | 1 |
193214
| `anyOf`, `allOf` | 1, plus their conditions |
194215
| `not` | 1, plus its condition |
195-
| An integer, a path, an `ifAbsent` or a size (`length`, `byteLength`, `count`) | 1 |
216+
| An integer, a path, an `ifAbsent`, a size (`length`, `byteLength`, `count`) or a time or height | 1 |
196217
| `add`, `multiply`, `subtract`, `divide`, `modulo`, `power` | 1, plus their operands |
197218

198219
`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.

0 commit comments

Comments
 (0)