@rootzero/contracts 1.20.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/Codec.sol +1 -1
  3. package/Commands.sol +1 -0
  4. package/Endpoints.sol +1 -0
  5. package/Events.sol +2 -2
  6. package/README.md +25 -15
  7. package/annotations/Schema.sol +35 -2
  8. package/codec/Blocks.sol +64 -2
  9. package/codec/Decoders.sol +71 -9
  10. package/codec/Descriptors.sol +28 -28
  11. package/codec/Readers.sol +39 -0
  12. package/codec/Schema.sol +45 -36
  13. package/codec/Specs.sol +9 -27
  14. package/codec/Writers.sol +8 -0
  15. package/commands/Allocate.sol +1 -1
  16. package/commands/Base.sol +3 -8
  17. package/commands/Burn.sol +1 -1
  18. package/commands/Credit.sol +1 -1
  19. package/commands/Debit.sol +1 -1
  20. package/commands/Deposit.sol +3 -10
  21. package/commands/Payout.sol +1 -1
  22. package/commands/Provision.sol +3 -3
  23. package/commands/Recover.sol +3 -3
  24. package/commands/Relay.sol +5 -4
  25. package/commands/Repay.sol +3 -3
  26. package/commands/Settle.sol +3 -3
  27. package/commands/Withdraw.sol +1 -1
  28. package/commands/admin/AllowAssets.sol +2 -2
  29. package/commands/admin/Allowance.sol +2 -2
  30. package/commands/admin/Annotate.sol +2 -2
  31. package/commands/admin/Appoint.sol +2 -2
  32. package/commands/admin/Authorize.sol +2 -2
  33. package/commands/admin/Base.sol +1 -1
  34. package/commands/admin/DenyAssets.sol +2 -8
  35. package/commands/admin/Dismiss.sol +2 -2
  36. package/commands/admin/Execute.sol +2 -2
  37. package/commands/admin/Unauthorize.sol +2 -2
  38. package/core/Host.sol +7 -10
  39. package/core/Portal.sol +11 -8
  40. package/events/Dispatch.sol +1 -1
  41. package/events/Relay.sol +1 -1
  42. package/events/Resolved.sol +17 -0
  43. package/events/Route.sol +1 -1
  44. package/events/Unresolved.sol +18 -0
  45. package/execution/Execution.sol +92 -5
  46. package/package.json +2 -3
  47. package/ports/AllowAssets.sol +1 -1
  48. package/ports/Allowance.sol +1 -1
  49. package/ports/Base.sol +3 -3
  50. package/ports/Credit.sol +1 -1
  51. package/ports/Debit.sol +1 -1
  52. package/ports/DenyAssets.sol +1 -1
  53. package/ports/Dispatch.sol +4 -2
  54. package/ports/Pipe.sol +2 -1
  55. package/ports/Post.sol +1 -1
  56. package/ports/Redeem.sol +1 -1
  57. package/docs/Schema.md +0 -403
  58. package/events/Recovered.sol +0 -17
  59. package/events/Undelivered.sol +0 -18
package/CHANGELOG.md CHANGED
@@ -3,6 +3,97 @@
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
+
63
+ ## 1.21.0
64
+
65
+ ### Breaking Changes
66
+
67
+ - Repacked endpoint descriptors as `[state key:4][stride:1]`,
68
+ `[input key:4][stride:1]`,
69
+ `[output key:4][min:4][max:4][hint:3][stride:1]`, four reserved bytes,
70
+ one transaction-count byte, and one flags byte. The input child-key field was
71
+ removed; indexers and request builders must decode the new layout.
72
+ - Removed container metadata and the `many` and `keys` helpers from `Specs`.
73
+ Top-level lists are now emitted custom schemas whose context-local key is the
74
+ endpoint input key; nested lists continue to use the generic `#list` key.
75
+ - Replaced the `funded` and `admin` boolean parameters on command helpers and
76
+ the `funded` boolean parameter on port helpers with a packed `uint8 flags`
77
+ argument. Endpoint flags now live in the dedicated `Flags` library.
78
+ - Renamed the portal lifecycle events from `Undelivered` and `Recovered` to
79
+ `Unresolved` and `Resolved`, and renamed `Portal.retry` to `Portal.resolve`.
80
+ `Portal` now composes the node-access, runtime, and lifecycle-event
81
+ capabilities; concrete implementations remain responsible for explicitly
82
+ emitting lifecycle events.
83
+
84
+ ### Added
85
+
86
+ - Added custom-spec overloads for `Decoders.list(cur, spec)` and
87
+ `Executions.list(exec, spec, lane)` so custom-keyed top-level lists can use
88
+ the same cursor model as generic nested lists.
89
+ - Added `Specs.lane` for canonical descriptor lane packing and exported
90
+ `Flags` from the codec, command, and endpoint package entry points.
91
+
92
+ ### Changed
93
+
94
+ - Updated schema and indexing documentation and the list example for the
95
+ custom-keyed top-level list convention.
96
+
6
97
  ## 1.20.0
7
98
 
8
99
  ### Breaking Changes
package/Codec.sol CHANGED
@@ -7,7 +7,7 @@ pragma solidity ^0.8.33;
7
7
  import { AssetAmount, AccountAsset, HostAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Position, Tx } from "./core/Types.sol";
8
8
  import { Keys } from "./codec/Keys.sol";
9
9
  import { Sizes, Specs } from "./codec/Specs.sol";
10
- import { Descriptors } from "./codec/Descriptors.sol";
10
+ import { Descriptors, Flags } from "./codec/Descriptors.sol";
11
11
  import { Schemas } from "./codec/Schema.sol";
12
12
  import { Decoders } from "./codec/Decoders.sol";
13
13
  import { Cursors, Cur } from "./utils/Cursors.sol";
package/Commands.sol CHANGED
@@ -5,6 +5,7 @@ pragma solidity ^0.8.33;
5
5
  // Import this file for both standard Execution-based commands and custom decoders.
6
6
 
7
7
  import {CommandBase} from "./commands/Base.sol";
8
+ import {Flags} from "./codec/Descriptors.sol";
8
9
  import {Execution, Executions} from "./execution/Execution.sol";
9
10
  import {Lanes} from "./utils/Lanes.sol";
10
11
  import {Blocks} from "./codec/Blocks.sol";
package/Endpoints.sol CHANGED
@@ -5,6 +5,7 @@ pragma solidity ^0.8.33;
5
5
  // Import this file to inherit from the full rootzero callable host surface without managing individual paths.
6
6
 
7
7
  // Shared endpoint hooks
8
+ import {Flags} from "./codec/Descriptors.sol";
8
9
  import {CreditAccountHook, DebitAccountHook, PostHook, RepayHook, SettleHook} from "./core/Settlement.sol";
9
10
 
10
11
  // Commands
package/Events.sol CHANGED
@@ -12,7 +12,7 @@ import { CommanderEvent } from "./events/Commander.sol";
12
12
  import { DispatchEvent } from "./events/Dispatch.sol";
13
13
  import { EndpointEvent } from "./events/Endpoint.sol";
14
14
  import { ReceivedEvent } from "./events/Received.sol";
15
- import { RecoveredEvent } from "./events/Recovered.sol";
15
+ import { ResolvedEvent } from "./events/Resolved.sol";
16
16
  import { RelayEvent } from "./events/Relay.sol";
17
17
  import { EventEmitter } from "./events/Emitter.sol";
18
18
  import { GuardianEvent } from "./events/Guardian.sol";
@@ -22,7 +22,7 @@ import { NodeEvent } from "./events/Node.sol";
22
22
  import { RootedEvent } from "./events/Rooted.sol";
23
23
  import { RouteEvent } from "./events/Route.sol";
24
24
  import { SpentEvent } from "./events/Spent.sol";
25
- import { UndeliveredEvent } from "./events/Undelivered.sol";
25
+ import { UnresolvedEvent } from "./events/Unresolved.sol";
26
26
  import { UnlockedEvent } from "./events/Unlocked.sol";
27
27
 
28
28
 
package/README.md CHANGED
@@ -114,24 +114,29 @@ 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
 
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.
131
+
124
132
  ## Batches
125
133
 
126
134
  A input is not a single struct; it is a run of blocks. One `#amount` block
127
135
  asks for one deposit, five blocks ask for five, and the code path is identical
128
136
  — every endpoint parses with a cursor and loops until the stream is exhausted.
129
137
  The descriptor lane key is the prime item: it is the block type that may repeat
130
- for batching. Plain lanes are encoded as `[key][0]`; readers interpret the zero
131
- group byte as group size 1 when the lane is non-empty. Generic list lanes such
132
- as `many #asset` are encoded as
133
- `[Keys.List][Keys.Asset]`, so indexers can see both the top-level LIST container
134
- and the item type inside it.
138
+ for batching. Readers interpret a zero group byte as group size 1 when the lane
139
+ is non-empty.
135
140
 
136
141
  Off-chain, building a batch is concatenation. Using the reference encoders from
137
142
  [`test/helpers/blocks.ts`](test/helpers/blocks.ts):
@@ -308,7 +313,7 @@ abstract contract MyCommand is CommandBase {
308
313
  uint private immutable descriptor;
309
314
 
310
315
  constructor() {
311
- (, descriptor) = command("myCommand", Specs.Empty, Specs.Amount, Specs.Balance, 0, false, false);
316
+ (, descriptor) = command("myCommand", Specs.Empty, Specs.Amount, Specs.Balance, 0, 0);
312
317
  }
313
318
 
314
319
  function myCommand(
@@ -327,6 +332,10 @@ abstract contract MyCommand is CommandBase {
327
332
  }
328
333
  ```
329
334
 
335
+ The final argument is a packed flags byte. Pass `0` for an ordinary endpoint,
336
+ or compose values such as `Flags.Funded`, `Flags.Admin`, and
337
+ `Flags.AdminFunded` from the command or endpoint package entry point.
338
+
330
339
  The standard commands cover the common ledger movements: `deposit` and
331
340
  `depositPayable` (external funds in), `settlePayable` (funded settlement),
332
341
  `withdraw` and `burn` (funds out),
@@ -466,12 +475,13 @@ Import from the package entry points rather than deep paths:
466
475
 
467
476
  - `@rootzero/contracts/Core.sol` — `Host`, access control, `Balances`,
468
477
  `Settlement`, `Pipeline`, `Portal`, validator
469
- - `@rootzero/contracts/Commands.sol` — `CommandBase`, `Execution`, codec
470
- helpers, and shared value types for authoring custom commands
478
+ - `@rootzero/contracts/Commands.sol` — `CommandBase`, `Execution`, `Flags`,
479
+ codec helpers, and shared value types for authoring custom commands
471
480
  - `@rootzero/contracts/Endpoints.sol` — command, admin, port, guard, and query
472
- mixins and their hooks
481
+ mixins, their hooks, and `Flags`
473
482
  - `@rootzero/contracts/Codec.sol` — `Blocks`, calldata `Cur`/`Cursors`, memory
474
- `Reader`/`Readers`, `Writers`, `Schemas`, `Keys`, and `Specs`
483
+ `Reader`/`Readers`, `Writers`, `Schemas`, `Descriptors`, `Flags`, `Keys`, and
484
+ `Specs`
475
485
  - `@rootzero/contracts/Utils.sol` — `Ids`, `Nodes`, `Assets`, `Accounts`,
476
486
  layout and value helpers
477
487
  - `@rootzero/contracts/Events.sol` — protocol event contracts
@@ -486,7 +496,7 @@ Repo layout:
486
496
  - `contracts/blocks` — block schema, cursor parsing, writers
487
497
  - `contracts/utils` — ids, nodes, assets, accounts, layout, ECDSA
488
498
  - `contracts/events` — event contracts and emitters
489
- - `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)
490
500
 
491
501
  Use this library to create a new rootzero host, implement a command, or reuse
492
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.
@@ -194,7 +250,15 @@ library Decoders {
194
250
  /// @param cur Cursor advanced past the list.
195
251
  /// @return items Cursor spanning the list payload.
196
252
  function list(Cur memory cur) internal pure returns (Cur memory items) {
197
- (uint abs, uint end) = consume(cur, Specs.List);
253
+ return list(cur, Specs.List);
254
+ }
255
+
256
+ /// @notice Consume a list block described by `spec` and return a cursor over its items.
257
+ /// @param cur Cursor advanced past the list.
258
+ /// @param spec Custom list block specification.
259
+ /// @return items Cursor spanning the list payload.
260
+ function list(Cur memory cur, uint spec) internal pure returns (Cur memory items) {
261
+ (uint abs, uint end) = consume(cur, spec);
198
262
  items.state = Cursors.create(abs, end - abs, 0, 0, 0);
199
263
  }
200
264
 
@@ -202,7 +266,7 @@ library Decoders {
202
266
  /// @param cur Cursor advanced past the block.
203
267
  /// @param key Expected block key.
204
268
  /// @return out Cursor spanning the complete encoded block.
205
- 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) {
206
270
  uint abs = cur.state.absolute();
207
271
  (, uint end) = consume(cur, Specs.create(key, 0, 0, 0));
208
272
  out.state = Cursors.create(abs, end - abs, 0, 0, 0);
@@ -248,9 +312,7 @@ library Decoders {
248
312
 
249
313
  /// @dev Return the next raw calldata word and advance by `size` bytes.
250
314
  function next(Cur memory cur, uint size) private pure returns (bytes32 value) {
251
- uint abs;
252
- (cur.state, abs) = cur.state.consume(size);
253
- value = Blocks.read32(abs);
315
+ value = Blocks.read32(take(cur, size));
254
316
  }
255
317
 
256
318
  /// @notice Return the next raw byte and advance the cursor by one byte.
@@ -4,25 +4,32 @@ pragma solidity ^0.8.33;
4
4
  import {Specs} from "./Specs.sol";
5
5
  import {Lanes} from "../utils/Lanes.sol";
6
6
 
7
+ /// @title Flags
8
+ /// @notice Packed endpoint behavior flags.
9
+ library Flags {
10
+ /// @dev Endpoint accepts nonzero native value.
11
+ uint8 internal constant Funded = 1 << 0;
12
+ /// @dev Endpoint is restricted to the admin account.
13
+ uint8 internal constant Admin = 1 << 1;
14
+ /// @dev Endpoint accepts nonzero native value and is restricted to the admin account.
15
+ uint8 internal constant AdminFunded = Admin | Funded;
16
+ }
17
+
7
18
  /// @title Descriptors
8
19
  /// @notice Packing and lane metadata helpers for endpoint descriptors.
9
20
  library Descriptors {
10
21
  /// @dev The requested lane is not part of an endpoint descriptor.
11
22
  error InvalidLane();
12
23
 
13
- /// @dev Endpoint accepts nonzero native value.
14
- uint8 internal constant Funded = 1 << 0;
15
- /// @dev Endpoint is restricted to the admin account.
16
- uint8 internal constant Admin = 1 << 1;
17
-
18
24
  /// @notice Create a descriptor from endpoint lane specifications and flags.
19
25
  /// @dev Layout: `[state key:4][stride:1]`
20
- /// `[input key:4][item:4][stride:1]`
26
+ /// `[input key:4][stride:1]`
21
27
  /// `[output key:4][min:4][max:4][hint:3][stride:1]`
28
+ /// `[reserved:4]`
22
29
  /// `[transactions:1]`
23
30
  /// `[flags:1]`. Flag bits: funded = 0, admin = 1.
24
31
  /// @param state State lane specification.
25
- /// @param input Input lane specification, optionally wrapped in a container.
32
+ /// @param input Direct input lane specification.
26
33
  /// @param output Output writer specification.
27
34
  /// @param transactions Transactions produced per batch.
28
35
  /// @param flags Endpoint behavior flags.
@@ -34,15 +41,13 @@ library Descriptors {
34
41
  uint8 transactions,
35
42
  uint8 flags
36
43
  ) internal pure returns (uint descriptor) {
37
- state = Specs.normalize(state, true);
38
- input = Specs.normalize(input, false);
39
- output = Specs.normalize(output, true);
44
+ output = Specs.normalize(output);
40
45
  descriptor = pack(state, input, output, transactions, flags);
41
46
  }
42
47
 
43
- /// @dev Pack normalized endpoint specs and flags into a descriptor.
44
- /// @param state Normalized direct state specification.
45
- /// @param input Normalized input specification.
48
+ /// @dev Pack endpoint specs and flags into a descriptor.
49
+ /// @param state State specification.
50
+ /// @param input Input specification.
46
51
  /// @param output Normalized direct output specification.
47
52
  /// @param transactions Transactions produced per batch.
48
53
  /// @param flags Endpoint behavior flags.
@@ -54,14 +59,9 @@ library Descriptors {
54
59
  uint8 transactions,
55
60
  uint8 flags
56
61
  ) private pure returns (uint descriptor) {
57
- (bytes4 outer, bytes4 child) = Specs.keys(input);
58
-
59
- descriptor |= uint(uint32(Specs.key(state))) << 224;
60
- descriptor |= uint(Specs.stride(state)) << 216;
61
- descriptor |= uint(uint32(outer)) << 184;
62
- descriptor |= uint(uint32(child)) << 152;
63
- descriptor |= uint(Specs.stride(input)) << 144;
64
- descriptor |= (output >> 128) << 16;
62
+ descriptor |= uint(Specs.lane(state)) << 216;
63
+ descriptor |= uint(Specs.lane(input)) << 176;
64
+ descriptor |= (output >> 128) << 48;
65
65
  descriptor |= uint(transactions) << 8;
66
66
  descriptor |= flags;
67
67
  }
@@ -82,22 +82,22 @@ library Descriptors {
82
82
  /// @return Effective blocks per batch for the lane.
83
83
  function stride(uint descriptor, uint8 lane) internal pure returns (uint8) {
84
84
  if (lane == Lanes.State) return uint8(descriptor >> 216);
85
- if (lane == Lanes.Input) return uint8(descriptor >> 144);
86
- if (lane == Lanes.Output) return uint8(descriptor >> 16);
85
+ if (lane == Lanes.Input) return uint8(descriptor >> 176);
86
+ if (lane == Lanes.Output) return uint8(descriptor >> 48);
87
87
  if (lane == Lanes.Transactions) return uint8(descriptor >> 8);
88
88
  revert InvalidLane();
89
89
  }
90
90
 
91
91
  /// @notice Return the effective block key for `lane`.
92
- /// @dev Input returns its outer key. Transaction blocks have a fixed
93
- /// protocol key that is not stored in the descriptor.
92
+ /// @dev Transaction blocks have a fixed protocol key that is not stored in
93
+ /// the descriptor.
94
94
  /// @param descriptor Packed endpoint descriptor.
95
95
  /// @param lane Lane identifier from `Lanes`.
96
- /// @return Effective outer block key for the lane.
96
+ /// @return Effective block key for the lane.
97
97
  function key(uint descriptor, uint8 lane) internal pure returns (bytes4) {
98
98
  if (lane == Lanes.State) return bytes4(uint32(descriptor >> 224));
99
99
  if (lane == Lanes.Input) return bytes4(uint32(descriptor >> 184));
100
- if (lane == Lanes.Output) return bytes4(uint32(descriptor >> 112));
100
+ if (lane == Lanes.Output) return bytes4(uint32(descriptor >> 144));
101
101
  if (lane == Lanes.Transactions) return Specs.key(Specs.Transaction);
102
102
  revert InvalidLane();
103
103
  }
@@ -110,7 +110,7 @@ library Descriptors {
110
110
  /// @return growable Whether the writer may grow beyond that capacity.
111
111
  function allocation(uint descriptor, uint8 lane, uint groups) internal pure returns (uint capacity, bool growable) {
112
112
  if (lane == Lanes.Output) {
113
- uint spec = uint(uint128(descriptor >> 16)) << 128;
113
+ uint spec = uint(uint128(descriptor >> 48)) << 128;
114
114
  return Specs.allocation(spec, groups);
115
115
  }
116
116
  if (lane == Lanes.Transactions) {