@rootzero/contracts 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,63 @@
3
3
  Until the protocol reaches integration-stable status, minor versions may include
4
4
  breaking API changes. Breaking changes are called out explicitly.
5
5
 
6
+ ## 1.22.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - Declared child blocks are now structurally present whenever their parent is
11
+ non-empty. A child without a value uses its zero-payload block form instead
12
+ of being omitted. The `maybe` modifier now hints that onchain code accepts
13
+ that empty form; it no longer describes an absent item. Lists likewise keep
14
+ their header and represent no items with an empty payload.
15
+ - Removed `Schemas.Unit`. Use the empty form of the block key that carries the
16
+ relevant semantic meaning instead of a generic unit marker.
17
+ - Renamed `Decoders.take(cur, key)` to `takeBlock(cur, key)`. The `take` name is
18
+ now used by raw cursor navigation and returns the absolute start of a
19
+ bounds-checked byte range while advancing the cursor.
20
+ - Normalized standard schema bodies by removing their presentation-only outer
21
+ braces, and renamed the standard node field from `id` to `node`. Published
22
+ schema annotation bytes therefore change even though their wire layouts do
23
+ not.
24
+
25
+ ### Added
26
+
27
+ - Added empty-block inspection, conditional consumption, encoding, writing,
28
+ and execution-output helpers across `Blocks`, `Decoders`, `Readers`,
29
+ `Writers`, and `Executions`.
30
+ - Added `absolute`, `advance`, and raw `take` cursor helpers, plus `enter`
31
+ overloads that advance over a validated fixed payload prefix in one cursor
32
+ update.
33
+ - Added unnamed `schema` overloads for context-local specifications and an
34
+ unnamed `Blocks.schema` factory overload.
35
+ - Added the offchain-only `at N` schema projection hint. Explicit positions are
36
+ reserved first, then unannotated siblings fill the remaining positions in
37
+ declaration order; wire encoding and onchain decoding remain unchanged.
38
+
39
+ ### Changed
40
+
41
+ - Clarified that one optional pair of outer braces is presentation-only for all
42
+ non-empty schema bodies, including custom top-level `many` schemas.
43
+ - Updated custom input examples to group fixed-width fields for direct calldata
44
+ reads, demonstrate empty child blocks, and separate top-level and nested swap
45
+ decoding.
46
+ - Excluded repository documentation and examples from the prepared npm package;
47
+ the package continues to contain Solidity sources, the README, changelog, and
48
+ license.
49
+
50
+ ### Upgrade Compatibility
51
+
52
+ - Emit every declared child header in schema order. When a `maybe` child has no
53
+ value, emit that child's key with a zero payload length and call
54
+ `tryConsumeEmpty` before its strict semantic unpacker.
55
+ - Replace `Schemas.Unit` markers with an empty block carrying the key expected
56
+ by the receiving schema.
57
+ - Replace `cur.take(key)` with `cur.takeBlock(key)`. Use `cur.take(amount)` only
58
+ for raw fixed-width ranges.
59
+ - Indexers should accept braced and unbraced schema bodies, treat `maybe` as an
60
+ empty-value hint, and apply `at N` only after decoding declaration-order wire
61
+ data.
62
+
6
63
  ## 1.21.0
7
64
 
8
65
  ### Breaking Changes
package/README.md CHANGED
@@ -114,16 +114,20 @@ identifiers inside, never how the bytes are laid out.
114
114
 
115
115
  Schemas can express more than flat fields: a block may contain any number of
116
116
  nested child blocks (`#bytes as payload` names raw dynamic bytes), items can be
117
- marked `maybe` (optional) or `many` (a list), and aliases and dotted field
118
- paths give off-chain tooling presentation names without changing a single byte
119
- on the wire. The full schema language is specified in
120
- [`docs/Schema.md`](docs/Schema.md). The standard block schemas live in
117
+ marked `maybe` when their empty form is accepted or `many` when they form a
118
+ list, and aliases and dotted field paths give off-chain tooling presentation
119
+ names without changing a single byte on the wire. Declared child headers are
120
+ always present; a zero payload length represents an empty block. An `at N` hint
121
+ can reposition one field in off-chain presentation without changing its wire
122
+ position. The full schema language is specified in
123
+ [`docs/Schema.md`](https://github.com/lastqubit/rootzero-evm/blob/main/docs/Schema.md). The standard block schemas live in
121
124
  `Schemas` and their runtime keys in `Keys` (both via
122
125
  `@rootzero/contracts/Codec.sol`).
123
126
 
124
- A rare top-level list is published as a custom schema such as `many #asset`.
125
- Its context-local schema key becomes the outer block key accepted by the
126
- endpoint; nested lists continue to use the generic `#list` key.
127
+ A rare top-level list is published as a custom schema consisting of one item,
128
+ such as `many #asset` or `{ many #asset }`. Its context-local schema key becomes
129
+ the outer block key accepted by the endpoint; a list alongside sibling items
130
+ continues to use the generic `#list` key.
127
131
 
128
132
  ## Batches
129
133
 
@@ -492,7 +496,7 @@ Repo layout:
492
496
  - `contracts/blocks` — block schema, cursor parsing, writers
493
497
  - `contracts/utils` — ids, nodes, assets, accounts, layout, ECDSA
494
498
  - `contracts/events` — event contracts and emitters
495
- - `docs` — [`Schema.md`](docs/Schema.md) (wire format and schema DSL)
499
+ - `docs` — [`Schema.md`](https://github.com/lastqubit/rootzero-evm/blob/main/docs/Schema.md) (wire format and schema DSL)
496
500
 
497
501
  Use this library to create a new rootzero host, implement a command, or reuse
498
502
  the protocol's block format in tooling. It is the shared protocol foundation,
@@ -11,6 +11,23 @@ import {Runtime} from "../core/Runtime.sol";
11
11
  /// @dev Schema annotations accumulate for distinct block keys. For a trusted
12
12
  /// emitter, the latest schema for the same block key replaces the earlier claim.
13
13
  abstract contract Schema is Runtime, AnnotationEvent {
14
+ /// @notice Construct and publish an unnamed context-local block specification.
15
+ /// @param key Context-local key value.
16
+ /// @param min Minimum accepted payload length.
17
+ /// @param max Maximum accepted payload length; zero means unbounded.
18
+ /// @param hint Initial per-block payload capacity.
19
+ /// @param body Schema DSL string describing the block payload body.
20
+ /// @return spec The context-local block specification.
21
+ function schema(
22
+ uint32 key,
23
+ uint32 min,
24
+ uint32 max,
25
+ uint32 hint,
26
+ string memory body
27
+ ) internal returns (uint spec) {
28
+ return schema(key, min, max, hint, body, bytes32(0));
29
+ }
30
+
14
31
  /// @notice Construct and publish a context-local block specification.
15
32
  /// @param key Context-local key value.
16
33
  /// @param min Minimum accepted payload length.
@@ -27,8 +44,16 @@ abstract contract Schema is Runtime, AnnotationEvent {
27
44
  string memory body,
28
45
  bytes32 name
29
46
  ) internal returns (uint spec) {
30
- spec = Specs.create(key, min, max, hint);
31
- return schema(spec, body, name);
47
+ return schema(Specs.create(key, min, max, hint), body, name);
48
+ }
49
+
50
+ /// @notice Construct and publish an unnamed exact-size context-local block specification.
51
+ /// @param key Context-local key value.
52
+ /// @param size Exact payload length and initial per-block payload capacity.
53
+ /// @param body Schema DSL string describing the block payload body.
54
+ /// @return spec The context-local block specification.
55
+ function schema(uint32 key, uint32 size, string memory body) internal returns (uint spec) {
56
+ return schema(key, size, body, bytes32(0));
32
57
  }
33
58
 
34
59
  /// @notice Construct and publish an exact-size context-local block specification.
@@ -41,6 +66,14 @@ abstract contract Schema is Runtime, AnnotationEvent {
41
66
  return schema(Specs.create(key, size), body, name);
42
67
  }
43
68
 
69
+ /// @notice Publish an unnamed, already constructed block specification for the current host.
70
+ /// @param spec Packed block specification.
71
+ /// @param body Schema DSL string describing the block payload body.
72
+ /// @return The published block specification.
73
+ function schema(uint spec, string memory body) internal returns (uint) {
74
+ return schema(spec, body, bytes32(0));
75
+ }
76
+
44
77
  /// @notice Publish an already constructed block specification for the current host.
45
78
  /// @param spec Packed block specification.
46
79
  /// @param body Schema DSL string describing the block payload body.
package/codec/Blocks.sol CHANGED
@@ -73,6 +73,17 @@ library Blocks {
73
73
  len = uint32(head >> 192);
74
74
  }
75
75
 
76
+ /// @notice Decode a complete block header within an absolute calldata region.
77
+ /// @param abs Absolute position of the header.
78
+ /// @param end Absolute region boundary.
79
+ /// @return key Decoded block key.
80
+ /// @return len Decoded payload length.
81
+ function peek(uint abs, uint end) internal pure returns (bytes4 key, uint len) {
82
+ if (abs > end || Sizes.Header > end - abs) revert MalformedBlocks();
83
+ (key, len) = header(abs);
84
+ if (len > end - abs - Sizes.Header) revert MalformedBlocks();
85
+ }
86
+
76
87
  /// @notice Validate a block header at an absolute calldata position.
77
88
  /// @dev DANGER: This performs an unchecked calldata read and does not ensure `end`
78
89
  /// lies within the caller's logical calldata region. Only the key, minimum,
@@ -101,6 +112,17 @@ library Blocks {
101
112
  end = body + size;
102
113
  }
103
114
 
115
+ /// @notice Validate an empty block at an absolute calldata position.
116
+ /// @dev DANGER: This performs an unchecked calldata read. The caller must
117
+ /// validate the returned end against its logical calldata region.
118
+ /// @param abs Absolute position of the block header.
119
+ /// @param key Expected block key.
120
+ /// @return end Absolute position immediately after the empty block header.
121
+ function expectEmpty(uint abs, bytes4 key) internal pure returns (uint end) {
122
+ if (header(abs, key) != 0) revert InvalidBlock();
123
+ return abs + Sizes.Header;
124
+ }
125
+
104
126
  /// @notice Return whether `abs` identifies a header with `key` before an absolute end.
105
127
  /// @param abs Absolute calldata position to inspect.
106
128
  /// @param end Absolute region boundary.
@@ -111,6 +133,16 @@ library Blocks {
111
133
  return bytes4(read32(abs)) == key;
112
134
  }
113
135
 
136
+ /// @notice Return whether `abs` identifies a complete empty block header.
137
+ /// @param abs Absolute position to inspect.
138
+ /// @param end Absolute region boundary.
139
+ /// @param key Expected block key.
140
+ /// @return Whether the expected key occurs with a zero-length payload.
141
+ function isEmpty(uint abs, uint end, bytes4 key) internal pure returns (bool) {
142
+ if (!hasAt(abs, end, key)) return false;
143
+ return uint32(uint(read32(abs)) >> 192) == 0;
144
+ }
145
+
114
146
  /// @notice Find the first block with `key` at or after absolute position `abs`.
115
147
  /// @param abs Absolute search position.
116
148
  /// @param end Absolute region boundary.
@@ -160,6 +192,18 @@ library Blocks {
160
192
 
161
193
  // Generic block writes
162
194
 
195
+ /// @notice Write an empty block header at `i`.
196
+ /// @dev DANGER: Unchecked memory write. Reserve `Sizes.Header` bytes first.
197
+ /// @param dst Destination buffer.
198
+ /// @param i Relative write position.
199
+ /// @param key Block key.
200
+ function writeEmpty(bytes memory dst, uint i, bytes4 key) internal pure {
201
+ uint head = uint(uint32(key)) << 224;
202
+ assembly ("memory-safe") {
203
+ mstore(add(add(dst, 0x20), i), head)
204
+ }
205
+ }
206
+
163
207
  /// @notice Write a custom block with one payload word at `i`.
164
208
  /// @dev DANGER: Unchecked memory write. Reserve `Sizes.B32` bytes first.
165
209
  /// @param dst Destination buffer.
@@ -1729,6 +1773,14 @@ library Blocks {
1729
1773
 
1730
1774
  // Generic factories
1731
1775
 
1776
+ /// @notice Encode an empty block.
1777
+ /// @param key Block type key.
1778
+ /// @return value Encoded empty block header.
1779
+ function empty(bytes4 key) internal pure returns (bytes memory value) {
1780
+ value = allocate(Sizes.Header);
1781
+ writeEmpty(value, 0, key);
1782
+ }
1783
+
1732
1784
  /// @notice Encode a block with a raw payload.
1733
1785
  /// @param key Block type key.
1734
1786
  /// @param payload Raw payload bytes.
@@ -1831,6 +1883,14 @@ library Blocks {
1831
1883
  /// @notice Encode a SCHEMA block.
1832
1884
  /// @param spec Block specification.
1833
1885
  /// @param body Schema body.
1886
+ /// @return value Encoded SCHEMA block bytes.
1887
+ function schema(uint spec, string memory body) internal pure returns (bytes memory value) {
1888
+ return schema(spec, body, bytes32(0));
1889
+ }
1890
+
1891
+ /// @notice Encode a named SCHEMA block.
1892
+ /// @param spec Block specification.
1893
+ /// @param body Schema body.
1834
1894
  /// @param name Schema name.
1835
1895
  /// @return value Encoded SCHEMA block bytes.
1836
1896
  function schema(uint spec, string memory body, bytes32 name) internal pure returns (bytes memory value) {
@@ -1931,7 +1991,8 @@ library Blocks {
1931
1991
  }
1932
1992
 
1933
1993
  /// @notice Encode a RELAY block.
1934
- /// @param portal Destination portal identifier, often the destination host ID.
1994
+ /// @param portal Destination portal implementation's host ID, passed through
1995
+ /// without semantic validation.
1935
1996
  /// @param resources Chain-specific resources for the destination context.
1936
1997
  /// @param input Nested input block stream.
1937
1998
  /// @return value Encoded RELAY block bytes.
@@ -1949,7 +2010,8 @@ library Blocks {
1949
2010
  }
1950
2011
 
1951
2012
  /// @notice Encode a DISPATCH block.
1952
- /// @param portal Destination portal identifier, often the destination host ID.
2013
+ /// @param portal Destination portal implementation's host ID, passed through
2014
+ /// without semantic validation.
1953
2015
  /// @param resources Chain-specific resources for the destination dispatch.
1954
2016
  /// @param payload Encoded payload.
1955
2017
  /// @return value Encoded DISPATCH block bytes.
@@ -52,6 +52,13 @@ library Decoders {
52
52
  return cur.state.more();
53
53
  }
54
54
 
55
+ /// @notice Return the cursor's current absolute calldata position.
56
+ /// @param cur Cursor to inspect.
57
+ /// @return Current absolute calldata position.
58
+ function absolute(Cur memory cur) internal pure returns (uint) {
59
+ return cur.state.absolute();
60
+ }
61
+
55
62
  /// @notice Validate and consume the next block from a cursor.
56
63
  /// @param cur Cursor advanced over the complete block.
57
64
  /// @param spec Expected block specification.
@@ -62,6 +69,18 @@ library Decoders {
62
69
  cur.state = cur.state.seekAbs(end);
63
70
  }
64
71
 
72
+ /// @notice Consume a matching empty block from a cursor when present.
73
+ /// @param cur Cursor advanced only when the matching block is empty.
74
+ /// @param key Expected block key.
75
+ /// @return Whether an empty block was consumed.
76
+ function tryConsumeEmpty(Cur memory cur, bytes4 key) internal pure returns (bool) {
77
+ (uint i, uint offset, uint size) = cur.state.decode();
78
+ (bytes4 current, uint len) = Blocks.peek(offset + i, offset + size);
79
+ if (current != key || len != 0) return false;
80
+ cur.state = cur.state.seek(i + Sizes.Header);
81
+ return true;
82
+ }
83
+
65
84
  /// @notice Validate and enter the payload of the next block in a cursor.
66
85
  /// @dev The cursor remains in its existing frame so callers can decode child
67
86
  /// blocks in place. Callers should prove complete payload consumption with
@@ -71,8 +90,38 @@ library Decoders {
71
90
  /// @return abs Absolute position of the first payload byte.
72
91
  /// @return end Absolute position immediately after the payload.
73
92
  function enter(Cur memory cur, uint spec) internal pure returns (uint abs, uint end) {
93
+ return enter(cur, spec, 0);
94
+ }
95
+
96
+ /// @notice Validate a parent block and advance over a fixed payload prefix.
97
+ /// @dev `amount` is relative to the payload start and cannot exceed the
98
+ /// current parent payload. The returned `abs` remains the payload start.
99
+ /// @param cur Cursor advanced over the block header and fixed prefix.
100
+ /// @param spec Expected parent block specification.
101
+ /// @param amount Number of initial payload bytes to advance over.
102
+ /// @return abs Absolute position of the first payload byte.
103
+ /// @return end Absolute position immediately after the payload.
104
+ function enter(Cur memory cur, uint spec, uint amount) internal pure returns (uint abs, uint end) {
74
105
  (abs, end) = Blocks.expect(cur.state.absolute(), spec);
75
- cur.state = cur.state.seekAbs(abs);
106
+ if (amount > end - abs) revert Blocks.InvalidBlock();
107
+ cur.state = cur.state.seekAbs(abs + amount);
108
+ }
109
+
110
+ /// @notice Advance a cursor by a raw byte count.
111
+ /// @dev No block header or schema is validated.
112
+ /// @param cur Cursor advanced by `amount` bytes.
113
+ /// @param amount Number of bytes to advance.
114
+ function advance(Cur memory cur, uint amount) internal pure {
115
+ cur.state = cur.state.advance(amount);
116
+ }
117
+
118
+ /// @notice Take a raw byte range from the cursor.
119
+ /// @dev No block header or schema is validated.
120
+ /// @param cur Cursor advanced by `amount` bytes.
121
+ /// @param amount Number of bytes to take.
122
+ /// @return abs Absolute position of the first taken byte.
123
+ function take(Cur memory cur, uint amount) internal pure returns (uint abs) {
124
+ (cur.state, abs) = cur.state.consume(amount);
76
125
  }
77
126
 
78
127
  /// @notice Require a decoder cursor to be at absolute position `abs`.
@@ -128,9 +177,7 @@ library Decoders {
128
177
  /// @return len Payload length.
129
178
  function peek(Cur memory cur, uint i) internal pure returns (bytes4 key, uint len) {
130
179
  (, uint offset, uint size) = cur.state.decode();
131
- if (i > size || Sizes.Header > size - i) revert Blocks.MalformedBlocks();
132
- (key, len) = Blocks.header(offset + i);
133
- if (len > size - i - Sizes.Header) revert Blocks.MalformedBlocks();
180
+ return Blocks.peek(offset + i, offset + size);
134
181
  }
135
182
 
136
183
  /// @notice Return the relative position immediately after the current block.
@@ -161,6 +208,15 @@ library Decoders {
161
208
  return Blocks.hasAt(offset + i, offset + len, key);
162
209
  }
163
210
 
211
+ /// @notice Return whether the current block has `key` and an empty payload.
212
+ /// @param cur Cursor positioned at a block.
213
+ /// @param key Expected block key.
214
+ /// @return Whether a complete matching empty block header occurs at the current position.
215
+ function isEmpty(Cur memory cur, bytes4 key) internal pure returns (bool) {
216
+ (uint i, uint offset, uint len) = cur.state.decode();
217
+ return Blocks.isEmpty(offset + i, offset + len, key);
218
+ }
219
+
164
220
  /// @notice Find `key` at or after relative position `i`.
165
221
  /// @param cur Cursor to search.
166
222
  /// @param i Relative search position.
@@ -210,7 +266,7 @@ library Decoders {
210
266
  /// @param cur Cursor advanced past the block.
211
267
  /// @param key Expected block key.
212
268
  /// @return out Cursor spanning the complete encoded block.
213
- function take(Cur memory cur, bytes4 key) internal pure returns (Cur memory out) {
269
+ function takeBlock(Cur memory cur, bytes4 key) internal pure returns (Cur memory out) {
214
270
  uint abs = cur.state.absolute();
215
271
  (, uint end) = consume(cur, Specs.create(key, 0, 0, 0));
216
272
  out.state = Cursors.create(abs, end - abs, 0, 0, 0);
@@ -256,9 +312,7 @@ library Decoders {
256
312
 
257
313
  /// @dev Return the next raw calldata word and advance by `size` bytes.
258
314
  function next(Cur memory cur, uint size) private pure returns (bytes32 value) {
259
- uint abs;
260
- (cur.state, abs) = cur.state.consume(size);
261
- value = Blocks.read32(abs);
315
+ value = Blocks.read32(take(cur, size));
262
316
  }
263
317
 
264
318
  /// @notice Return the next raw byte and advance the cursor by one byte.
package/codec/Readers.sol CHANGED
@@ -45,6 +45,45 @@ library Readers {
45
45
  return cur.i != cur.source.length;
46
46
  }
47
47
 
48
+ /// @notice Return whether the current block has `key` and an empty payload.
49
+ /// @param cur Reader inspected without advancing.
50
+ /// @param key Expected block key.
51
+ /// @return Whether a complete matching empty block header occurs at the current position.
52
+ function isEmpty(Reader memory cur, bytes4 key) internal pure returns (bool) {
53
+ bytes memory source = cur.source;
54
+ uint i = cur.i;
55
+ if (i > source.length || source.length - i < Sizes.Header) return false;
56
+
57
+ uint header;
58
+ assembly ("memory-safe") {
59
+ header := mload(add(add(source, 0x20), i))
60
+ }
61
+ return bytes4(uint32(header >> 224)) == key && uint32(header >> 192) == 0;
62
+ }
63
+
64
+ /// @notice Consume a matching empty block from the reader when present.
65
+ /// @param cur Reader advanced only when the matching block is empty.
66
+ /// @param key Expected block key.
67
+ /// @return Whether an empty block was consumed.
68
+ function tryConsumeEmpty(Reader memory cur, bytes4 key) internal pure returns (bool) {
69
+ bytes memory source = cur.source;
70
+ uint i = cur.i;
71
+ if (i > source.length || source.length - i < Sizes.Header) revert InvalidBlock();
72
+
73
+ bytes4 current;
74
+ uint len;
75
+ assembly ("memory-safe") {
76
+ let header := mload(add(add(source, 0x20), i))
77
+ current := header
78
+ len := and(shr(192, header), 0xffffffff)
79
+ }
80
+
81
+ if (len > source.length - i - Sizes.Header) revert InvalidBlock();
82
+ if (current != key || len != 0) return false;
83
+ cur.i += Sizes.Header;
84
+ return true;
85
+ }
86
+
48
87
  /// @notice Validate and consume the current block, advancing `cur.i` past it.
49
88
  /// @param cur Reader to advance.
50
89
  /// @param key Expected block key.
package/codec/Schema.sol CHANGED
@@ -8,24 +8,34 @@ pragma solidity ^0.8.33;
8
8
  //
9
9
  // Schema:
10
10
  // - block aliases are published separately from payload schemas
11
- // - payload schemas are `""`, `{ fields }`, or a top-level `many #x`
11
+ // - payload schemas are `""` or a comma-separated item sequence; one optional
12
+ // pair of outer braces may wrap a non-empty sequence without changing meaning
12
13
  // - an empty schema string means the block has no structured payload
13
14
  // - commas separate siblings at every level
14
- // - braces define the current block payload body
15
+ // - braces are presentation-only and do not change payload layout
15
16
  // - command inputs are a single run when the input schema is non-empty
16
17
  // - command state is a single active state run without trailing globals
17
18
  // - run items may repeat at top level for batching
18
- // - `maybe #x` marks an optional block item
19
- // - nested `many #x` emits one generic list block containing repeated `#x` items
20
- // - a custom schema consisting of top-level `many #x` uses its custom key for
21
- // the outer list block and contains repeated `#x` items directly
19
+ // - every declared child block header is present when its parent is non-empty
20
+ // - any block may use a zero-length payload as its empty form
21
+ // - `maybe #x` hints that the onchain consumer accepts the empty form of `#x`
22
+ // when emptiness is not already intrinsic to the referenced block type
23
+ // - `many #x` alongside other items emits one generic list block containing
24
+ // zero or more repeated `#x` items; the list header is always present
25
+ // - a custom schema consisting of exactly one `many #x` item uses its custom
26
+ // key for the outer list block and contains repeated `#x` items directly,
27
+ // whether or not the item is wrapped in braces
22
28
  // - endpoint descriptor lanes identify their top-level block key directly
23
- // - `portal` fields are routing identifiers, often destination host IDs
29
+ // - `portal` fields identify destination portal hosts. By convention the value
30
+ // is the portal implementation's host ID; core passes it through unchanged
31
+ // and hooks may validate or resolve it for their transport
24
32
  // - `resources` fields are chain-specific resource words. A portal adapter
25
33
  // interprets them for the destination runtime. EVM resources use the low
26
34
  // 128 bits as native value.
27
35
  // - dotted field names and aliases, e.g. `dst.portal` or `#bytes as dst.payload`,
28
36
  // are offchain projection metadata only and do not change runtime encoding
37
+ // - `at N` assigns an offchain presentation position to one sibling; explicit
38
+ // positions are reserved first and unannotated siblings retain relative order
29
39
  // - child blocks resolve by alias in the active schema context; unresolved aliases are invalid
30
40
  // - schema strings describe the payload body only; the `Block` event carries the alias
31
41
  // - items are encoded in declaration order
@@ -63,7 +73,6 @@ pragma solidity ^0.8.33;
63
73
  library Schemas {
64
74
  // Empty and reserved payloads
65
75
 
66
- string constant Unit = "";
67
76
  string constant Bytes = "";
68
77
  string constant String = "";
69
78
  string constant List = "";
@@ -71,47 +80,47 @@ library Schemas {
71
80
 
72
81
  // One-word payloads
73
82
 
74
- string constant Node = "{ uint id }";
75
- string constant Account = "{ bytes32 account }";
76
- string constant Asset = "{ bytes32 asset }";
77
- string constant Status = "{ uint code }";
83
+ string constant Node = "uint node";
84
+ string constant Account = "bytes32 account";
85
+ string constant Asset = "bytes32 asset";
86
+ string constant Status = "uint code";
78
87
 
79
88
  // Two-word payloads
80
89
 
81
- string constant Amount = "{ bytes32 asset, uint amount }";
82
- string constant Balance = "{ bytes32 asset, uint amount }";
83
- string constant AccountAsset = "{ bytes32 account, bytes32 asset }";
84
- string constant HostAsset = "{ uint host, bytes32 asset }";
90
+ string constant Amount = "bytes32 asset, uint amount";
91
+ string constant Balance = "bytes32 asset, uint amount";
92
+ string constant AccountAsset = "bytes32 account, bytes32 asset";
93
+ string constant HostAsset = "uint host, bytes32 asset";
85
94
 
86
95
  // Three-word payloads
87
96
 
88
- string constant Allocation = "{ uint host, bytes32 asset, uint amount }";
89
- string constant Allowance = "{ uint host, bytes32 asset, uint amount }";
90
- string constant Custody = "{ uint host, bytes32 asset, uint amount }";
91
- string constant AccountAmount = "{ bytes32 account, bytes32 asset, uint amount }";
92
- string constant HostAmount = "{ uint host, bytes32 asset, uint amount }";
93
- string constant HostAccountAsset = "{ uint host, bytes32 account, bytes32 asset }";
97
+ string constant Allocation = "uint host, bytes32 asset, uint amount";
98
+ string constant Allowance = "uint host, bytes32 asset, uint amount";
99
+ string constant Custody = "uint host, bytes32 asset, uint amount";
100
+ string constant AccountAmount = "bytes32 account, bytes32 asset, uint amount";
101
+ string constant HostAmount = "uint host, bytes32 asset, uint amount";
102
+ string constant HostAccountAsset = "uint host, bytes32 account, bytes32 asset";
94
103
 
95
104
  // Four-word payloads
96
105
 
97
- string constant Position = "{ bytes32 asset, uint amount, bytes32 liability, uint debt }";
98
- string constant Transaction = "{ bytes32 from, bytes32 to, bytes32 asset, uint amount }";
99
- string constant HostAccountAmount = "{ uint host, bytes32 account, bytes32 asset, uint amount }";
106
+ string constant Position = "bytes32 asset, uint amount, bytes32 liability, uint debt";
107
+ string constant Transaction = "bytes32 from, bytes32 to, bytes32 asset, uint amount";
108
+ string constant HostAccountAmount = "uint host, bytes32 account, bytes32 asset, uint amount";
100
109
 
101
110
  // Composite payloads
102
111
 
103
- string constant Call = "{ uint target, uint resources, #bytes as payload }";
104
- string constant Step = "{ uint cmd, uint resources, #bytes as input }";
105
- string constant Relay = "{ uint portal, uint resources, #bytes as input }";
106
- string constant Dispatch = "{ uint portal, uint resources, #bytes as payload }";
107
- string constant Context = "{ bytes32 account, #bytes as state, #bytes as input }";
108
- string constant Recover = "{ uint handler, uint resources, bytes32 key, #bytes as witness }";
109
- string constant Annotation = "{ uint entity, #bytes as data }";
112
+ string constant Call = "uint target, uint resources, #bytes as payload";
113
+ string constant Step = "uint cmd, uint resources, #bytes as input";
114
+ string constant Relay = "uint portal, uint resources, #bytes as input";
115
+ string constant Dispatch = "uint portal, uint resources, #bytes as payload";
116
+ string constant Context = "bytes32 account, #bytes as state, #bytes as input";
117
+ string constant Recover = "uint handler, uint resources, bytes32 key, #bytes as witness";
118
+ string constant Annotation = "uint entity, #bytes as data";
110
119
 
111
120
  // Annotation payloads
112
121
 
113
- string constant Action = "{ uint action }";
114
- string constant Label = "{ bytes32 namespace, #string as name }";
115
- string constant Schema = "{ uint spec, #string as body, bytes32 name }";
122
+ string constant Action = "uint action";
123
+ string constant Label = "bytes32 namespace, #string as name";
124
+ string constant Schema = "uint spec, #string as body, bytes32 name";
116
125
  }
117
126
 
package/codec/Writers.sol CHANGED
@@ -73,6 +73,14 @@ library Writers {
73
73
  // Append helpers
74
74
  // -------------------------------------------------------------------------
75
75
 
76
+ /// @notice Append an empty block.
77
+ /// @param writer Destination writer.
78
+ /// @param key Block key.
79
+ function appendEmpty(Writer memory writer, bytes4 key) internal pure {
80
+ uint i = reserve(writer, Sizes.Header);
81
+ Blocks.writeEmpty(writer.dst, i, key);
82
+ }
83
+
76
84
  /// @notice Append arbitrary bytes to the writer.
77
85
  /// @param writer Destination writer; `i` is advanced by `data.length`.
78
86
  /// @param data Bytes to append.
@@ -8,7 +8,8 @@ using Executions for Execution;
8
8
  /// @notice Hook implemented by hosts that relay command contexts.
9
9
  abstract contract RelayPayableHook {
10
10
  /// @notice Override to relay a command context to `portal`.
11
- /// @param portal Destination portal identifier, often the destination host ID.
11
+ /// @param portal Destination portal implementation's host ID. Implementations
12
+ /// may validate or resolve it for their transport.
12
13
  /// @param resources Chain-specific destination resources. EVM adapters
13
14
  /// may interpret this as packed execution gas and destination value.
14
15
  /// @param account Destination command account.
package/core/Host.sol CHANGED
@@ -61,16 +61,19 @@ abstract contract CommandHost is CommanderAccess, CallerAccess, HostIntroduction
61
61
  function enforceCaller(address caller) internal view virtual override returns (address) {
62
62
  return enforceCommander(caller);
63
63
  }
64
-
65
64
  }
66
65
 
67
66
  /// @title Admins
68
67
  /// @notice Optional bundle of the default host administration commands.
69
- abstract contract Admins is Annotate, ExecutePayable, Authorize, Unauthorize {}
68
+ abstract contract Admins is Annotate, ExecutePayable, Authorize, Unauthorize {
69
+
70
+ }
70
71
 
71
72
  /// @title Guardians
72
73
  /// @notice Optional bundle for guardian management and the default revoke guard.
73
- abstract contract Guardians is Appoint, Dismiss, Revoke {}
74
+ abstract contract Guardians is Appoint, Dismiss, Revoke {
75
+
76
+ }
74
77
 
75
78
  /// @title Host
76
79
  /// @notice Abstract base contract for rootzero host implementations.
@@ -78,13 +81,7 @@ abstract contract Guardians is Appoint, Dismiss, Revoke {}
78
81
  /// guardian management, the default guardian revoke action, and
79
82
  /// optionally introduces itself to a commander host at deployment.
80
83
  /// Accepts native ETH payments via the `receive` function.
81
- abstract contract Host is
82
- Admins,
83
- Guardians,
84
- HostIntroduction,
85
- IntroductionEvent,
86
- IHostIntroduction
87
- {
84
+ abstract contract Host is Admins, Guardians, HostIntroduction, IntroductionEvent, IHostIntroduction {
88
85
  /// @param cmdr Commander address; used by the composed access capabilities.
89
86
  /// If `cmdr` is a deployed contract, the host calls `introduce`
90
87
  /// on it during construction.
@@ -8,7 +8,7 @@ abstract contract DispatchEvent is EventEmitter {
8
8
  string private constant ABI = "event Dispatch(uint indexed host, uint portal, uint resources, bytes32 key, bytes32 digest)";
9
9
 
10
10
  /// @param host Host node ID that owns the dispatch.
11
- /// @param portal Destination portal identifier, often the destination host ID.
11
+ /// @param portal Destination portal implementation's host ID.
12
12
  /// @param resources Chain-specific resources assigned to the dispatch.
13
13
  /// @param key Dispatch correlation or recovery lookup key.
14
14
  /// @param digest Digest of the dispatched payload or canonical envelope.
package/events/Relay.sol CHANGED
@@ -8,7 +8,7 @@ abstract contract RelayEvent is EventEmitter {
8
8
  string private constant ABI = "event Relay(bytes32 indexed account, uint portal, uint resources, bytes32 key, bytes32 digest)";
9
9
 
10
10
  /// @param account Account that owns the relayed context.
11
- /// @param portal Destination portal identifier, often the destination host ID.
11
+ /// @param portal Destination portal implementation's host ID.
12
12
  /// @param resources Chain-specific resources assigned to the relay.
13
13
  /// @param key Relay correlation or recovery lookup key.
14
14
  /// @param digest Digest of the relayed payload or canonical envelope.
package/events/Route.sol CHANGED
@@ -8,7 +8,7 @@ abstract contract RouteEvent is EventEmitter {
8
8
  string private constant ABI = "event Route(uint indexed host, uint portal, uint status)";
9
9
 
10
10
  /// @param host Host node ID that owns the route.
11
- /// @param portal Destination portal identifier, often the destination host ID.
11
+ /// @param portal Destination portal implementation's host ID.
12
12
  /// @param status Route status. Zero means inactive; nonzero means active.
13
13
  event Route(uint indexed host, uint portal, uint status);
14
14
 
@@ -187,6 +187,25 @@ library Executions {
187
187
  return exec.decoders.any();
188
188
  }
189
189
 
190
+ /// @notice Return a decoder lane's current absolute calldata position.
191
+ /// @param exec Execution whose decoder is inspected.
192
+ /// @param lane Decoder lane to select.
193
+ /// @return Current absolute calldata position of the selected lane.
194
+ function absolute(Execution memory exec, uint8 lane) internal pure returns (uint) {
195
+ return exec.decoders.select(lane).absolute();
196
+ }
197
+
198
+ /// @notice Return whether the next block on `lane` has `key` and an empty payload.
199
+ /// @param exec Execution whose decoder is inspected without advancing.
200
+ /// @param lane Decoder lane to inspect.
201
+ /// @param key Expected block key.
202
+ /// @return Whether a complete matching empty block header occurs next.
203
+ function isEmpty(Execution memory exec, uint8 lane, bytes4 key) internal pure returns (bool) {
204
+ uint cur = exec.decoders.select(lane);
205
+ (uint i, uint offset, uint len) = cur.decode();
206
+ return Blocks.isEmpty(offset + i, offset + len, key);
207
+ }
208
+
190
209
  /// @notice Validate and consume the next block from an execution decoder lane.
191
210
  /// @param exec Execution whose selected decoder cursor is advanced over the complete block.
192
211
  /// @param lane Execution decoder lane to select.
@@ -199,6 +218,20 @@ library Executions {
199
218
  exec.decoders = cur.seekAbs(end);
200
219
  }
201
220
 
221
+ /// @notice Consume a matching empty block from an execution decoder lane when present.
222
+ /// @param exec Execution whose selected decoder advances only for a matching empty block.
223
+ /// @param lane Decoder lane to consume.
224
+ /// @param key Expected block key.
225
+ /// @return Whether an empty block was consumed.
226
+ function tryConsumeEmpty(Execution memory exec, uint8 lane, bytes4 key) internal pure returns (bool) {
227
+ uint cur = exec.decoders.select(lane);
228
+ (uint i, uint offset, uint size) = cur.decode();
229
+ (bytes4 current, uint len) = Blocks.peek(offset + i, offset + size);
230
+ if (current != key || len != 0) return false;
231
+ exec.decoders = cur.seek(i + Sizes.Header);
232
+ return true;
233
+ }
234
+
202
235
  /// @notice Validate and enter the payload of the next block in an execution decoder lane.
203
236
  /// @dev The selected lane remains in its existing frame so callers can decode
204
237
  /// child blocks in place. Callers should prove complete payload consumption
@@ -209,9 +242,48 @@ library Executions {
209
242
  /// @return abs Absolute position of the first payload byte.
210
243
  /// @return end Absolute position immediately after the payload.
211
244
  function enter(Execution memory exec, uint8 lane, uint spec) internal pure returns (uint abs, uint end) {
245
+ return enter(exec, lane, spec, 0);
246
+ }
247
+
248
+ /// @notice Validate a parent block and advance over a fixed payload prefix.
249
+ /// @dev `amount` is relative to the payload start and cannot exceed the
250
+ /// current parent payload. The returned `abs` remains the payload start.
251
+ /// @param exec Execution whose selected decoder advances over the header and fixed prefix.
252
+ /// @param lane Execution decoder lane to select.
253
+ /// @param spec Expected parent block specification.
254
+ /// @param amount Number of initial payload bytes to advance over.
255
+ /// @return abs Absolute position of the first payload byte.
256
+ /// @return end Absolute position immediately after the payload.
257
+ function enter(
258
+ Execution memory exec,
259
+ uint8 lane,
260
+ uint spec,
261
+ uint amount
262
+ ) internal pure returns (uint abs, uint end) {
212
263
  uint cur = exec.decoders.select(lane);
213
264
  (abs, end) = Blocks.expect(cur.absolute(), spec);
214
- exec.decoders = cur.seekAbs(abs);
265
+ if (amount > end - abs) revert Blocks.InvalidBlock();
266
+ exec.decoders = cur.seekAbs(abs + amount);
267
+ }
268
+
269
+ /// @notice Advance an execution decoder lane by a raw byte count.
270
+ /// @dev No block header or schema is validated.
271
+ /// @param exec Execution whose selected decoder cursor is advanced.
272
+ /// @param lane Decoder lane to select.
273
+ /// @param amount Number of bytes to advance.
274
+ function advance(Execution memory exec, uint8 lane, uint amount) internal pure {
275
+ uint cur = exec.decoders.select(lane);
276
+ exec.decoders = cur.advance(amount);
277
+ }
278
+
279
+ /// @notice Take a raw byte range from an execution decoder lane.
280
+ /// @dev No block header or schema is validated.
281
+ /// @param exec Execution whose selected decoder cursor is advanced.
282
+ /// @param lane Decoder lane to select.
283
+ /// @param amount Number of bytes to take.
284
+ /// @return abs Absolute position of the first taken byte.
285
+ function take(Execution memory exec, uint8 lane, uint amount) internal pure returns (uint abs) {
286
+ (exec.decoders, abs) = exec.decoders.consume(lane, amount);
215
287
  }
216
288
 
217
289
  /// @notice Require the active execution decoder to be at absolute position `abs`.
@@ -246,9 +318,7 @@ library Executions {
246
318
 
247
319
  /// @dev Return the next raw calldata word from `lane` and advance by `size` bytes.
248
320
  function next(Execution memory exec, uint8 lane, uint size) private pure returns (bytes32 value) {
249
- uint abs;
250
- (exec.decoders, abs) = exec.decoders.consume(lane, size);
251
- value = Blocks.read32(abs);
321
+ value = Blocks.read32(take(exec, lane, size));
252
322
  }
253
323
 
254
324
  /// @notice Return the next raw byte from a decoder lane and advance it by one byte.
@@ -806,6 +876,14 @@ library Executions {
806
876
  return reserve(exec, size, size);
807
877
  }
808
878
 
879
+ /// @notice Append an empty block to execution output.
880
+ /// @param exec Execution receiving the block.
881
+ /// @param key Block key.
882
+ function outputEmpty(Execution memory exec, bytes4 key) internal pure {
883
+ uint i = reserve(exec, Sizes.Header);
884
+ Blocks.writeEmpty(exec.output, i, key);
885
+ }
886
+
809
887
  /// @notice Append an ACCOUNT block to execution output.
810
888
  /// @param exec Execution receiving the block.
811
889
  /// @param account Account identifier to encode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootzero/contracts",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Solidity contracts and protocol building blocks for rootzero hosts and commands.",
5
5
  "private": false,
6
6
  "license": "GPL-3.0-only",
@@ -9,8 +9,7 @@
9
9
  "**/*.sol",
10
10
  "README.md",
11
11
  "CHANGELOG.md",
12
- "LICENSE",
13
- "docs/Schema.md"
12
+ "LICENSE"
14
13
  ],
15
14
  "publishConfig": {
16
15
  "access": "public"
@@ -11,7 +11,8 @@ using Executions for Execution;
11
11
  /// @notice Hook implemented by hosts that forward funded dispatch payloads.
12
12
  abstract contract DispatchPayableHook {
13
13
  /// @notice Override to dispatch an encoded payload to `portal`.
14
- /// @param portal Destination portal identifier, often the destination host ID.
14
+ /// @param portal Destination portal implementation's host ID. Implementations
15
+ /// may validate or resolve it for their transport.
15
16
  /// @param resources Chain-specific destination resources. EVM adapters
16
17
  /// may interpret this as packed execution gas and destination value.
17
18
  /// @param payload Encoded payload ready for the transport layer.
package/docs/Schema.md DELETED
@@ -1,427 +0,0 @@
1
- # Schema
2
-
3
- Rootzero input and response data is encoded as a stream of typed blocks. A
4
- schema string describes the payload body for discovery events and tooling; the
5
- runtime block key is the compact type tag that identifies that payload layout in
6
- the active schema context. The block alias is published separately from the
7
- payload schema.
8
-
9
- ## Wire Format
10
-
11
- Every block uses the same header:
12
-
13
- ```txt
14
- [bytes4 key][uint32 payloadLen][payload]
15
- ```
16
-
17
- `payloadLen` is big-endian and counts only payload bytes. Child blocks and list
18
- items use the same header format.
19
-
20
- Standard built-in block keys use:
21
-
22
- ```txt
23
- bytes4(keccak256("#name"))
24
- ```
25
-
26
- For example, the standard `amount` alias uses the key derived from `#amount`
27
- and the schema body `{ bytes32 asset, uint amount }`. Custom block keys do not
28
- have to be keccak-derived. They
29
- are opaque `bytes4` tags and only need to be unique in the context where they are
30
- used. A host can publish the meaning of a custom key as an annotation:
31
-
32
- ```solidity
33
- event Annotation(uint indexed entity, bytes data);
34
- #schema { uint spec, #string as body, bytes32 name }
35
- ```
36
-
37
- Annotation merge behavior is defined by the annotation block type rather than
38
- by the `Annotation` event. A `#schema` annotation is identified by its entity
39
- and the block key encoded in `spec`: distinct keys accumulate, while the latest
40
- trusted claim for the same key replaces the earlier one. Other annotation types
41
- may define additive, historical, or explicitly revocable behavior instead.
42
-
43
- The standard `#action { uint action }` annotation assigns one primary semantic
44
- action to an entity. The latest trusted value replaces the previous value, and
45
- `Actions.None` clears the classification.
46
-
47
- For example, a host-specific payment block can use a small literal, the command
48
- selector, or any other chosen `bytes4` value as long as that key is not
49
- overloaded in the relevant host/schema context.
50
-
51
- ## Block Syntax
52
-
53
- A block definition has an event alias and a schema body. A schema body is one
54
- of three forms:
55
-
56
- ```txt
57
- "" empty or raw payload
58
- { fields } structured payload
59
- many #item top-level custom list payload
60
- ```
61
-
62
- Fixed fields are written in braces:
63
-
64
- ```txt
65
- alias: amount
66
- schema: { bytes32 asset, uint amount }
67
- ```
68
-
69
- A block body can reference another block alias as a child item with `#`:
70
-
71
- ```txt
72
- { bytes32 account, #bytes as state, #bytes as input }
73
- ```
74
-
75
- The empty schema string `""` means the block has no structured payload. This is
76
- used for zero-payload blocks such as `#unit` and raw dynamic blocks such as
77
- `#bytes`. A root `many #item` body is reserved for an emitted custom schema
78
- whose own key identifies the outer list block.
79
-
80
- A structured schema body is a comma-separated list of items. Order is
81
- significant.
82
-
83
- ```txt
84
- { #amount, maybe #account as recipient }
85
- ```
86
-
87
- ## Payload Layout
88
-
89
- A block payload encodes schema items in declaration order. Fixed fields are
90
- packed inline, and zero or more child blocks are embedded directly at their
91
- declared positions. Because every child block carries its own header and payload
92
- length, fixed fields may appear before, after, or between child blocks.
93
-
94
- ```txt
95
- { uint target, uint resources, #bytes as payload }
96
- { bytes32 account, #bytes as state, #bytes as input }
97
- { uint spec, #string as body, bytes32 name }
98
- { #bytes as left, uint op, #bytes as right }
99
- ```
100
-
101
- There is no wrapper around embedded child blocks.
102
-
103
- Raw dynamic bytes are represented with the reserved `#bytes` child block. Use an
104
- alias to give those bytes a presentation name:
105
-
106
- ```txt
107
- #bytes as payload
108
- ```
109
-
110
- ## Modifiers
111
-
112
- Cardinality is expressed with prefix keywords:
113
-
114
- ```txt
115
- #balance
116
- maybe #balance
117
- many #balance
118
- maybe many #balance
119
- ```
120
-
121
- - no prefix: one required item
122
- - `maybe`: optional item
123
- - `many`: a list whose payload contains repeated items
124
- - `maybe many`: optional `#list` block
125
-
126
- `maybe` emits no placeholder when absent. Inside a structured schema body,
127
- `many` wraps repeated items in one generic `#list` block; it does not repeat the
128
- item in place:
129
-
130
- ```txt
131
- { uint id, many #asset as assets }
132
- ```
133
-
134
- When an emitted custom schema consists entirely of a top-level `many` item, the
135
- custom schema key identifies the outer list block instead. Its payload contains
136
- the repeated items directly:
137
-
138
- ```txt
139
- schema key: 0x00000001
140
- schema body: many #asset
141
- wire value: [0x00000001][length][ASSET][ASSET]...
142
- ```
143
-
144
- This convention gives a top-level list a discoverable, context-local type while
145
- retaining the generic `#list` key for lists whose type is supplied by an
146
- enclosing schema.
147
-
148
- ## Endpoint Lanes
149
-
150
- Endpoint descriptors identify each lane with a block key and group size. In
151
- Solidity, endpoint definition helpers accept block specs such as `Specs.Amount`.
152
- A zero group is interpreted as group size 1, while `Specs.Empty` means the
153
- endpoint has no blocks in that lane. Use `group(spec, size)` when a lane needs
154
- an explicit group size other than 1.
155
-
156
- The packed descriptor uses these lane layouts:
157
-
158
- ```txt
159
- state [key:4][group:1]
160
- input [key:4][group:1]
161
- output [key:4][min:4][max:4][hint:3][group:1]
162
- reserve [reserved:4]
163
- tx [transactions:1]
164
- flags [flags:1]
165
- ```
166
-
167
- Each lane directly identifies its top-level block key. Output lanes retain their
168
- size bounds and allocation hint so execution can reconstruct the output spec and
169
- initialize its writer directly. Four descriptor-level bytes are reserved after
170
- the lane metadata. The Solidity output decoder returns a left-aligned,
171
- writer-ready spec that retains its encoded group and clears its reserved fields.
172
- `Specs.group` returns the effective group, interpreting an encoded zero as one
173
- for a non-empty spec.
174
-
175
- Any non-empty lane resolves its key to a block alias and schema body through the
176
- active schema context. A top-level list lane uses the key of its emitted custom
177
- `many` schema; the descriptor treats it like every other direct lane spec.
178
-
179
- The lane key is the prime item. Prime items may repeat at the top level for
180
- batching. Later top-level items are globals for the whole batch and are not
181
- counted as per-operation prime blocks.
182
-
183
- The prime item cannot be optional. If an endpoint needs a per-operation marker
184
- with no payload, use a zero-payload block such as `#unit`.
185
-
186
- Endpoint descriptors currently use a narrower convention than the full block
187
- grammar: each state, input, or output lane is a single run of blocks, without
188
- additional global items. Endpoint decoder opening requires that run to consume
189
- the complete supplied lane; a trailing block with another key is invalid.
190
- Lower-level cursor scanning may still intentionally open only a prefix run.
191
- Those lower layers retain only the raw block count; descriptor strides are
192
- applied and lane groups reconciled once when an endpoint execution opens.
193
- Future protocol surfaces may use the more flexible top-level structure.
194
-
195
- For commands, complete-lane validation is also a state-safety rule. State is a
196
- linear value owned by the current pipeline step, not optional context that a
197
- command may disregard. Every command must account for the complete supplied
198
- state by consuming it, transforming and returning it, forwarding it intact, or
199
- reverting. A command whose descriptor declares an empty state lane must reject
200
- non-empty state. A command that accepts state must validate the complete stream
201
- against its declared schema; accepting only a prefix and silently dropping the
202
- remainder is invalid.
203
-
204
- ## Live Pipeline State
205
-
206
- `#balance`, `#custody`, and `#position` are live state carried between command
207
- steps for the active account. A position atomically pairs an asset side with a
208
- liability side:
209
-
210
- ```txt
211
- position { bytes32 asset, uint amount, bytes32 liability, uint debt }
212
- ```
213
-
214
- The pair is deliberately general. The asset side represents value acquired or
215
- controlled, and the liability side represents value owed or required. Commands
216
- may preserve or replace either side and return a new position. The terminal
217
- `settle` command consumes the pair. Position state is transient protocol state;
218
- rewriting it does not by itself create, discharge, or replace an obligation
219
- persisted by a host or external protocol. The responsible command hook must
220
- perform or verify those effects. A command must not ignore a supplied position:
221
- it must explicitly consume, transform, forward, or reject it, so neither its
222
- asset nor its debt can disappear accidentally.
223
-
224
- This representation supports ordinary forward transformations as well as
225
- backward composition. For example, an exact-output route can carry its desired
226
- asset while successive hops replace the upstream liability:
227
-
228
- ```txt
229
- position(C, 100, C, 100)
230
- → position(C, 100, B, 50)
231
- → position(C, 100, A, 25)
232
- → settle
233
- ```
234
-
235
- “Backward” describes how requirements are composed from the desired result
236
- toward the source. Pipeline execution is not reversed: STEP blocks always run
237
- forward in their encoded order. Exact-output routing is only an example;
238
- borrowing, refinancing, collateral transformation, callback obligations,
239
- cross-host claims, fees, and netting can use the same position state.
240
-
241
- ## Field Aliases
242
-
243
- Block aliases are published in `#schema` annotations. Field aliases are presentation
244
- metadata for tooling. They do not change payload layout or runtime keys.
245
-
246
- ```txt
247
- maybe #account as recipient
248
- { uint target, uint resources, #bytes as payload }
249
- ```
250
-
251
- Field aliases may be used on any block item, including child blocks and prime
252
- items.
253
-
254
- Child blocks are schema references:
255
-
256
- ```txt
257
- { uint handler, uint resources, bytes32 key, #bytes as witness }
258
- ```
259
-
260
- Alias resolution is context-dependent. A consumer may resolve `#context` from
261
- standard block events, from app-specific block events, or from another active
262
- schema context. Custom parents should define nested custom blocks from the
263
- bottom up and reference them by alias. Consumers should reject schemas with
264
- unresolved aliases. The runtime encoding is still an embedded child block with
265
- the referenced key and layout.
266
-
267
- ## Field Paths
268
-
269
- Field names and aliases may use dotted paths for offchain projection. A dotted
270
- path does not change the block key, payload bytes, payload length, cursor
271
- behavior, or any onchain validation. It is metadata only.
272
-
273
- ```txt
274
- { uint dst.portal, uint dst.resources, #bytes as dst.payload }
275
- ```
276
-
277
- This has the same runtime layout as:
278
-
279
- ```txt
280
- { uint portal, uint resources, #bytes as payload }
281
- ```
282
-
283
- Offchain tooling may decode the dotted form into a nested object:
284
-
285
- ```ts
286
- {
287
- dst: {
288
- portal,
289
- resources,
290
- payload
291
- }
292
- }
293
- ```
294
-
295
- Encoding and decoding must still follow schema declaration order, not object
296
- property order. Fields with the same path prefix do not need to be contiguous,
297
- although contiguous fields are easier to read when they represent one logical
298
- object.
299
-
300
- Tooling should reject duplicate full paths and prefix/value collisions:
301
-
302
- ```txt
303
- uint dst.portal, uint dst.portal // duplicate path
304
- uint dst, uint dst.portal // prefix/value collision
305
- ```
306
-
307
- The same rule applies to field aliases:
308
-
309
- ```txt
310
- { uint target, uint resources, #bytes as calldata.payload }
311
- maybe #account as recipient.account
312
- ```
313
-
314
- ## Field Types
315
-
316
- Supported field types are chain-neutral:
317
-
318
- ```txt
319
- uint, uint8, uint16, uint32, uint64, uint128, uint256
320
- int, int8, int16, int32, int64, int128, int256
321
- bool
322
- bytes1 through bytes32
323
- ```
324
-
325
- `uint` means `uint256`; `int` means `int256`. Other integer widths, unsized
326
- `bytes`, `string`, and array syntax are not part of the core schema DSL.
327
-
328
- Restricting fixed bytes to the power-of-two widths `bytes1`, `bytes2`,
329
- `bytes4`, `bytes8`, `bytes16`, and `bytes32` is under consideration, but has
330
- not been decided. Until that decision is made, the schema DSL continues to
331
- allow every `bytesN` width from 1 through 32.
332
-
333
- Integers are encoded big-endian. Signed integers use two's-complement encoding
334
- for their declared width. `bool` is one byte: `0x00` for false and `0x01` for
335
- true. `bytesN` values are encoded as exactly `N` bytes with no padding.
336
-
337
- ## Chain Resources
338
-
339
- Fields named `portal` are routing identifiers; they are often the destination
340
- host ID, but a transport adapter may define a different stable handle.
341
-
342
- Fields named `resources` are chain-specific resource words. A portal adapter
343
- interprets them for the destination runtime. Different runtimes may pack these
344
- words differently, but a given runtime must use one stable format everywhere.
345
- For EVM chains, the low 128 bits are native value / endowment in wei; higher
346
- bits are reserved for execution resources such as gas.
347
-
348
- ## Protocol IDs
349
-
350
- Account, asset, and node ID fields use one 32-byte convention:
351
-
352
- - first byte `0x00`: opaque ID, encoded as `0x00 || bytes31(hash)`. The full
353
- preimage must come from a lookup table or witness data when native metadata is
354
- needed.
355
- - first byte nonzero: structured ID. The value may be deconstructed according
356
- to its chain/runtime layout.
357
-
358
- Opaque preimages must start with a one-byte format/hash tag; `0x01` means
359
- keccak256. The remaining bytes are host/domain-specific until the protocol
360
- standardizes a fuller preimage payload format.
361
-
362
- The field name supplies the protocol role for opaque IDs. For example, a
363
- `bytes32 asset` whose first byte is zero is still an asset in that block; it
364
- just cannot be decoded without external context. Runtime helpers that inspect
365
- the layout of an ID only apply to structured IDs.
366
-
367
- ## Identifiers
368
-
369
- Block aliases use lower camelCase ASCII identifiers. Field names and aliases
370
- use one or more lower camelCase path segments separated by dots:
371
-
372
- ```txt
373
- [a-z][a-zA-Z0-9]*
374
- [a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)*
375
- ```
376
-
377
- Invalid examples:
378
-
379
- ```txt
380
- Amount
381
- asset_meta
382
- asset-meta
383
- 0account
384
- asset.
385
- .asset
386
- ```
387
-
388
- Reserved words include `maybe`, `many`, `as`, all field type names, and the
389
- reserved block aliases `bytes` and `list`. For dotted paths, reserved words are
390
- invalid in any path segment.
391
-
392
- ## Reserved Blocks
393
-
394
- - `#bytes`: raw dynamic bytes, written without a body
395
- - `#string`: UTF-8 string bytes, written without a body
396
- - `#list`: generic list wrapper emitted by nested `many`
397
-
398
- Custom input shapes should define their own context-local block spec and publish
399
- it with a `#schema` annotation. Endpoint contracts can use `schema(...)` to
400
- construct and publish that spec:
401
-
402
- ```solidity
403
- uint input = schema(1, 64, 64, 64, "{ bytes32 asset, uint amount }", bytes32(0));
404
- ```
405
-
406
- Use different numeric keys when a host needs more than one local block key. The
407
- key can also be a selector or any other `bytes4` value that is unique in the
408
- context where it is used. The numeric arguments after the key are the minimum,
409
- maximum, and allocation hint payload sizes. The alias names the block; the
410
- schema string describes only the payload body.
411
-
412
- ## Standard Blocks
413
-
414
- Common protocol schemas live in `contracts/codec/Schema.sol`:
415
-
416
- ```txt
417
- amount { bytes32 asset, uint amount }
418
- balance { bytes32 asset, uint amount }
419
- custody { uint host, bytes32 asset, uint amount }
420
- call { uint target, uint resources, #bytes as payload }
421
- step { uint cmd, uint resources, #bytes as input }
422
- context { bytes32 account, #bytes as state, #bytes as input }
423
- recover { uint handler, uint resources, bytes32 key, #bytes as witness }
424
- schema { uint spec, #string as body, bytes32 name }
425
- ```
426
-
427
- `Keys.sol` contains the corresponding standard runtime keys.