@rootzero/contracts 1.9.0 → 1.11.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 (63) hide show
  1. package/CHANGELOG.md +40 -2
  2. package/Core.sol +3 -1
  3. package/Endpoints.sol +3 -6
  4. package/Events.sol +3 -6
  5. package/README.md +35 -33
  6. package/Utils.sol +2 -1
  7. package/blocks/Cursors.sol +39 -88
  8. package/blocks/Keys.sol +14 -4
  9. package/blocks/Schema.sol +49 -44
  10. package/commands/Base.sol +43 -13
  11. package/commands/Burn.sol +3 -6
  12. package/commands/Credit.sol +9 -6
  13. package/commands/Debit.sol +15 -11
  14. package/commands/Deposit.sol +19 -23
  15. package/commands/Payout.sol +6 -10
  16. package/commands/Provision.sol +19 -23
  17. package/commands/Recover.sol +16 -10
  18. package/commands/Relay.sol +21 -12
  19. package/commands/Withdraw.sol +3 -6
  20. package/commands/admin/AllowAssets.sol +7 -10
  21. package/commands/admin/Allowance.sol +7 -10
  22. package/commands/admin/Appoint.sol +7 -10
  23. package/commands/admin/Authorize.sol +7 -10
  24. package/commands/admin/Base.sol +1 -2
  25. package/commands/admin/DenyAssets.sol +7 -10
  26. package/commands/admin/Dismiss.sol +7 -10
  27. package/commands/admin/Execute.sol +7 -10
  28. package/commands/admin/Label.sol +8 -11
  29. package/commands/admin/Unauthorize.sol +7 -10
  30. package/core/Endpoint.sol +153 -0
  31. package/core/Payable.sol +7 -0
  32. package/core/Pipeline.sol +1 -1
  33. package/core/Portal.sol +19 -48
  34. package/docs/Schema.md +114 -82
  35. package/events/Endpoint.sol +19 -0
  36. package/events/Recovered.sol +17 -0
  37. package/events/Schema.sol +23 -0
  38. package/guards/Base.sol +17 -8
  39. package/guards/Revoke.sol +4 -6
  40. package/package.json +1 -1
  41. package/ports/AllowAssets.sol +6 -9
  42. package/ports/Allowance.sol +6 -9
  43. package/ports/Base.sol +21 -8
  44. package/ports/Credit.sol +6 -9
  45. package/ports/Debit.sol +6 -9
  46. package/ports/DenyAssets.sol +6 -9
  47. package/ports/Dispatch.sol +7 -10
  48. package/ports/Pipe.sol +4 -7
  49. package/ports/Redeem.sol +4 -7
  50. package/ports/Settle.sol +6 -9
  51. package/queries/Assets.sol +9 -12
  52. package/queries/Balances.sol +7 -9
  53. package/queries/Base.sol +19 -11
  54. package/utils/Selectors.sol +49 -0
  55. package/commands/admin/Destroy.sol +0 -43
  56. package/commands/admin/Init.sol +0 -43
  57. package/events/Admin.sol +0 -32
  58. package/events/Command.sol +0 -32
  59. package/events/Guard.sol +0 -18
  60. package/events/Port.sol +0 -22
  61. package/events/Query.sol +0 -20
  62. package/events/Resolved.sol +0 -17
  63. package/queries/Positions.sol +0 -54
package/CHANGELOG.md CHANGED
@@ -3,6 +3,44 @@
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.11.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - Replaced the separate command, port, query, guard, and admin discovery events
11
+ with the shared `Endpoint` event and a packed endpoint descriptor. Admin
12
+ commands are now identified by the descriptor admin flag. Default lane groups
13
+ remain encoded as zero and are interpreted as group size one when read; the
14
+ final four descriptor bytes are reserved.
15
+ - Renamed the block discovery event to `Schema` and added a `name` field for
16
+ alias-style schema references.
17
+ - Removed the generic `DATA` block type. Custom input blocks should define
18
+ their own local keys and publish schemas explicitly when they need discovery.
19
+ - Removed the built-in admin `init` and `destroy` commands.
20
+ - Removed the built-in `Positions` query; custom position output should be
21
+ modeled as a custom query.
22
+ - Changed `Cursors.list` to consume the LIST block and return a cursor scoped
23
+ to its payload instead of returning the list's end offset.
24
+ - Renamed the `CommandContext.request` field to `input`. The tuple layout and
25
+ command selectors are unchanged.
26
+
27
+ ## 1.10.0
28
+
29
+ ### Breaking Changes
30
+
31
+ - Moved `RecoverHook` from `Portal.sol` to `commands/Recover.sol`.
32
+ - Moved `RoutePayableHook` from `Portal.sol` to `commands/Relay.sol`.
33
+ - Renamed Portal helpers to `forward` / `retry`; hosts now explicitly bridge
34
+ the `RecoverPayable` hook to Portal retry behavior.
35
+ - Renamed the generic `Resolved` event to `Recovered`.
36
+ - Portal no longer emits `Recovered` when retrying an undelivered witness.
37
+ - Renamed `Cursors.exit` to `ensureAt` and renamed its position argument to
38
+ `pos`.
39
+ - Removed the redundant `next` return value from both `Cursors.init` overloads;
40
+ callers should use the returned cursor's `len` as the run boundary.
41
+ - Changed `Cursors.list` to require the expected current cursor position as
42
+ `pos` before entering the LIST block.
43
+
6
44
  ## 1.9.0
7
45
 
8
46
  ### Breaking Changes
@@ -18,10 +56,10 @@ breaking API changes. Breaking changes are called out explicitly.
18
56
  so routes can be removed by emitting zero status.
19
57
  - Added `Undelivered(uint indexed host, bytes32 key, bytes32 digest)` for portal
20
58
  messages that could not be delivered to their handler port.
21
- - Added `Resolved(uint indexed host, bytes32 key)` as the
59
+ - Added `Recovered(uint indexed host, bytes32 key)` as the
22
60
  matching event for resolved undelivered digests.
23
61
  - Removed the generic `Commitments` core mixin and `Commitment` event in favor
24
- of domain-specific events such as `Undelivered` and `Resolved`.
62
+ of domain-specific events such as `Undelivered` and `Recovered`.
25
63
 
26
64
  ## 1.8.0
27
65
 
package/Core.sol CHANGED
@@ -10,9 +10,11 @@ import { Escrows, InsufficientEscrow } from "./core/Escrows.sol";
10
10
  import { NativeAsset, Runtime } from "./core/Runtime.sol";
11
11
  import { Host, IHostIntroduction } from "./core/Host.sol";
12
12
  import { CommandCalls, FailedCall, NodeCalls, PortCalls } from "./core/Calls.sol";
13
+ import { EndpointBase, Lane } from "./core/Endpoint.sol";
13
14
  import { Payable } from "./core/Payable.sol";
14
15
  import { Pipeline } from "./core/Pipeline.sol";
15
- import { Portal, RecoverHook, RoutePayableHook } from "./core/Portal.sol";
16
+ import { Portal } from "./core/Portal.sol";
17
+ import { RecoverHook } from "./commands/Recover.sol";
16
18
  import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx } from "./core/Types.sol";
17
19
  import { Validator } from "./core/Validator.sol";
18
20
 
package/Endpoints.sol CHANGED
@@ -7,6 +7,7 @@ pragma solidity ^0.8.33;
7
7
  // Shared helpers
8
8
  import { Keys } from "./blocks/Keys.sol";
9
9
  import { CommandBase, CommandContext } from "./commands/Base.sol";
10
+ import { EndpointBase, Lane } from "./core/Endpoint.sol";
10
11
  import { Payable } from "./core/Payable.sol";
11
12
 
12
13
  // Commands
@@ -16,9 +17,8 @@ import { DebitAccount, DebitAccountHook } from "./commands/Debit.sol";
16
17
  import { Deposit, DepositHook, DepositPayable, DepositPayableHook } from "./commands/Deposit.sol";
17
18
  import { Payout, PayoutHook } from "./commands/Payout.sol";
18
19
  import { Provision, ProvisionHook, ProvisionPayable, ProvisionPayableHook } from "./commands/Provision.sol";
19
- import { RecoverPayable } from "./commands/Recover.sol";
20
- import { RelayPayable } from "./commands/Relay.sol";
21
- import { RecoverHook, RoutePayableHook } from "./core/Portal.sol";
20
+ import { RecoverHook, RecoverPayable } from "./commands/Recover.sol";
21
+ import { RelayPayable, RoutePayableHook } from "./commands/Relay.sol";
22
22
  import { Withdraw, WithdrawHook } from "./commands/Withdraw.sol";
23
23
 
24
24
  // Admin commands
@@ -27,11 +27,9 @@ import { AllowAssets, AllowAssetsHook } from "./commands/admin/AllowAssets.sol";
27
27
  import { Allowance, AllowanceHook } from "./commands/admin/Allowance.sol";
28
28
  import { Appoint } from "./commands/admin/Appoint.sol";
29
29
  import { Authorize } from "./commands/admin/Authorize.sol";
30
- import { Destroy, DestroyHook } from "./commands/admin/Destroy.sol";
31
30
  import { DenyAssets, DenyAssetsHook } from "./commands/admin/DenyAssets.sol";
32
31
  import { Dismiss } from "./commands/admin/Dismiss.sol";
33
32
  import { ExecutePayable } from "./commands/admin/Execute.sol";
34
- import { Init, InitHook } from "./commands/admin/Init.sol";
35
33
  import { Label } from "./commands/admin/Label.sol";
36
34
  import { Unauthorize } from "./commands/admin/Unauthorize.sol";
37
35
 
@@ -55,4 +53,3 @@ import { Revoke } from "./guards/Revoke.sol";
55
53
  import { QueryBase } from "./queries/Base.sol";
56
54
  import { AssetStatus, AssetStatusHook } from "./queries/Assets.sol";
57
55
  import { GetBalances, GetBalancesHook } from "./queries/Balances.sol";
58
- import { GetPosition, GetPositionHook } from "./queries/Positions.sol";
package/Events.sol CHANGED
@@ -4,27 +4,24 @@ pragma solidity ^0.8.33;
4
4
  // Aggregator: re-exports all event contracts.
5
5
  // Import this file to get access to every event emitter in one import.
6
6
 
7
- import { AdminEvent } from "./events/Admin.sol";
8
7
  import { AssetEvent, AssetStatusEvent } from "./events/Asset.sol";
9
8
  import { Actions } from "./utils/Actions.sol";
10
9
  import { BalanceEvent } from "./events/Balance.sol";
11
10
  import { CommanderEvent } from "./events/Commander.sol";
12
- import { CommandEvent } from "./events/Command.sol";
13
11
  import { DispatchEvent } from "./events/Dispatch.sol";
12
+ import { EndpointEvent } from "./events/Endpoint.sol";
14
13
  import { PositionEvent } from "./events/Position.sol";
15
14
  import { ReceivedEvent } from "./events/Received.sol";
16
- import { ResolvedEvent } from "./events/Resolved.sol";
15
+ import { RecoveredEvent } from "./events/Recovered.sol";
17
16
  import { EventEmitter } from "./events/Emitter.sol";
18
- import { GuardEvent } from "./events/Guard.sol";
19
17
  import { GuardianEvent } from "./events/Guardian.sol";
20
18
  import { IntroductionEvent } from "./events/Introduction.sol";
21
19
  import { LabeledEvent } from "./events/Labeled.sol";
22
20
  import { LockedEvent } from "./events/Locked.sol";
23
21
  import { NodeEvent } from "./events/Node.sol";
24
- import { PortEvent } from "./events/Port.sol";
25
- import { QueryEvent } from "./events/Query.sol";
26
22
  import { RootedEvent } from "./events/Rooted.sol";
27
23
  import { RouteEvent } from "./events/Route.sol";
24
+ import { SchemaEvent } from "./events/Schema.sol";
28
25
  import { SpentEvent } from "./events/Spent.sol";
29
26
  import { UndeliveredEvent } from "./events/Undelivered.sol";
30
27
  import { UnlockedEvent } from "./events/Unlocked.sol";
package/README.md CHANGED
@@ -58,7 +58,7 @@ const host = await ethers.deployContract("ExampleHost", [deployer.address]);
58
58
 
59
59
  const account = encodeUserAccount(user.address); // receiving account
60
60
  const request = encodeAmountBlock(asset, 100n); // what to deposit
61
- await host.deposit({ account, state: "0x", request }); // emits Balance
61
+ await host.deposit({ account, state: "0x", input: request }); // emits Balance
62
62
  ```
63
63
 
64
64
  The rest of this guide explains the ideas this example leans on — blocks, IDs,
@@ -74,11 +74,12 @@ payload:
74
74
  [bytes4 key][uint32 payloadLen][payload]
75
75
  ```
76
76
 
77
- The key is `bytes4(keccak256("#name"))`, and the payload layout is described by
78
- a schema string. For example, the block that requests a deposit:
77
+ The key is usually `bytes4(keccak256("#name"))`, and the payload layout is
78
+ described by a schema body published under an alias. For example, the standard
79
+ `amount` block that requests a deposit:
79
80
 
80
81
  ```txt
81
- #amount { bytes32 asset, uint amount }
82
+ amount { bytes32 asset, uint amount }
82
83
  ```
83
84
 
84
85
  is 72 bytes on the wire: an 8-byte header followed by two big-endian 32-byte
@@ -102,8 +103,12 @@ on the wire. The full schema language is specified in
102
103
  A request is not a single struct; it is a run of blocks. One `#amount` block
103
104
  asks for one deposit, five blocks ask for five, and the code path is identical
104
105
  — every endpoint parses with a cursor and loops until the stream is exhausted.
105
- The first item of a schema (the *prime item*) is the one that may repeat;
106
- later top-level items, if any, apply to the whole batch.
106
+ The descriptor lane key is the prime item: it is the block type that may repeat
107
+ for batching. Plain lanes are encoded as `[key][0]`; readers interpret the zero
108
+ group byte as group size 1 when the lane is non-empty. Generic list lanes such
109
+ as `many #asset` are encoded as
110
+ `[Keys.List][Keys.Asset]`, so indexers can see both the top-level LIST container
111
+ and the item type inside it.
107
112
 
108
113
  Off-chain, building a batch is concatenation. Using the reference encoders from
109
114
  [`test/helpers/blocks.ts`](test/helpers/blocks.ts):
@@ -207,49 +212,46 @@ Commands are the write endpoints. Every command receives the same context:
207
212
  struct CommandContext {
208
213
  bytes32 account; // acting account
209
214
  bytes state; // block stream produced by the previous command
210
- bytes request; // block stream for this invocation
215
+ bytes input; // block stream for this invocation
211
216
  }
212
217
  ```
213
218
 
214
- The request carries instructions; the state carries live value. While a
219
+ The input carries instructions; the state carries live value. While a
215
220
  sequence of commands executes, `#balance` and `#custody` blocks in the state
216
221
  are the funds being moved — produced by one command, consumed by the next.
217
222
 
218
- The standard `Deposit` mixin shows the canonical shape — init a cursor, loop
219
- the batch, call the hook, write the output run:
223
+ The standard `Deposit` mixin shows the canonical shape — open the input,
224
+ loop the batch, call the hook, write the output run:
220
225
 
221
226
  ```solidity
222
227
  function deposit(CommandContext calldata c) external onlyCommand returns (bytes memory) {
223
- (Cur memory request, uint groups, ) = Cursors.init(c.request, 1);
224
- Writer memory writer = Writers.allocBalances(groups);
228
+ (Cur memory input, uint outputs) = openInput(c.input, descriptor);
229
+ Writer memory output = Writers.allocBalances(outputs);
225
230
 
226
- while (request.i < request.len) {
227
- (bytes32 asset, uint amount) = request.unpackAmount();
231
+ while (input.i < input.len) {
232
+ (bytes32 asset, uint amount) = input.unpackAmount();
228
233
  deposit(c.account, asset, amount); // host policy hook
229
- writer.appendBalance(asset, amount);
234
+ output.appendBalance(asset, amount);
230
235
  }
231
236
 
232
- request.complete();
233
- return writer.finish();
237
+ return output.finish();
234
238
  }
235
239
  ```
236
240
 
237
241
  A command announces itself when the host is deployed. Its constructor emits a
238
- discovery event carrying the request schema, the expected and produced state
239
- block keys, and a shape string (`"1:0:1"` = one request block per operation, no
240
- input state, one output block per operation), plus a human-readable label:
242
+ discovery event carrying a packed descriptor with the input, state, and output
243
+ lanes, derived group sizes, and flags, plus a human-readable label:
241
244
 
242
245
  ```solidity
243
246
  abstract contract MyCommand is CommandBase {
244
- uint internal immutable myCommandId = commandId(this.myCommand.selector);
247
+ bytes32 private immutable descriptor;
245
248
 
246
249
  constructor() {
247
- emit Command(host, myCommandId, "1:0:1", Schemas.Amount, Keys.Empty, Keys.Balance, false);
248
- emit Labeled(myCommandId, bytes32(0), "myCommand");
250
+ (, descriptor) = command("myCommand", Keys.Empty, Keys.Amount, Keys.Balance, 0, false, false);
249
251
  }
250
252
 
251
253
  function myCommand(CommandContext calldata c) external onlyCommand returns (bytes memory) {
252
- // parse c.request, loop, return the output state run
254
+ // parse c.input, loop, return the output state run
253
255
  }
254
256
  }
255
257
  ```
@@ -266,7 +268,7 @@ A single command is rarely the whole story. A pipeline is a run of `#step`
266
268
  blocks executed in order within one transaction:
267
269
 
268
270
  ```txt
269
- #step { uint target, uint resources, #bytes as request }
271
+ step { uint target, uint resources, #bytes as request }
270
272
  ```
271
273
 
272
274
  Each step names a target command, the resources it may spend, and its request.
@@ -297,22 +299,22 @@ standard `getBalances` query takes a run of positions and answers each one in
297
299
  order:
298
300
 
299
301
  ```txt
300
- request: #accountAsset { bytes32 account, bytes32 asset }
301
- response: #accountAmount { bytes32 account, bytes32 asset, uint amount }
302
+ request: accountAsset { bytes32 account, bytes32 asset }
303
+ response: accountAmount { bytes32 account, bytes32 asset, uint amount }
302
304
  ```
303
305
 
304
- Like commands, every query announces its request and response schemas at
305
- deployment, so tooling knows how to call it without artifacts.
306
+ Like commands, every query announces a descriptor at deployment; tooling resolves
307
+ the descriptor's lanes through the published block schemas.
306
308
 
307
309
  ## Ports
308
310
 
309
311
  Ports are the host-to-host surfaces, callable only by trusted peer hosts. The two
310
312
  central ones are batches all the way down:
311
313
 
312
- - `portSettle` consumes `#transaction { bytes32 from, bytes32 to, bytes32 asset,
314
+ - `portSettle` consumes `transaction { bytes32 from, bytes32 to, bytes32 asset,
313
315
  uint amount }` blocks, debiting `from` and crediting `to` per
314
316
  block — how two hosts record settlement between their ledgers.
315
- - `portPipePayable` consumes `#context` blocks, each carrying an account, an
317
+ - `portPipePayable` consumes `context` blocks, each carrying an account, an
316
318
  initial state, and a run of steps — a complete pipeline delivered by another
317
319
  host, executed locally against the port call's shared value budget.
318
320
 
@@ -339,8 +341,8 @@ drop a trusted node immediately.
339
341
  ## Events and Discovery
340
342
 
341
343
  Hosts are self-describing. At deployment a host emits the ABI of every event it
342
- uses (`EventAbi`), a discovery event per endpoint with its full schemas, and
343
- labels for human-readable names. State changes then follow evented
344
+ uses (`EventAbi`), block schema events, endpoint descriptors, and labels for
345
+ human-readable names. State changes then follow evented
344
346
  conventions: `Balance` for every ledger change and flow events (`Received`,
345
347
  `Spent`, `Locked`, `Unlocked`) for value movement, each tagged with the endpoint that
346
348
  caused it. An indexer can reconstruct the entire repository — endpoints,
package/Utils.sol CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  pragma solidity ^0.8.33;
3
3
 
4
- // Aggregator: re-exports all utility libraries (Keys, Accounts, Actions, Assets, ECDSA, Ids, Nodes, Layout, Utils, Value).
4
+ // Aggregator: re-exports all utility libraries (Keys, Accounts, Actions, Assets, ECDSA, Ids, Nodes, Selectors, Layout, Utils, Value).
5
5
  // Import this file to access the full utility surface without managing individual paths.
6
6
 
7
7
  import { Keys } from "./blocks/Keys.sol";
@@ -11,6 +11,7 @@ import { Amounts, Assets } from "./utils/Assets.sol";
11
11
  import { ECDSA } from "./utils/ECDSA.sol";
12
12
  import { Ids } from "./utils/Ids.sol";
13
13
  import { Nodes } from "./utils/Nodes.sol";
14
+ import { Selectors } from "./utils/Selectors.sol";
14
15
  import { Layout } from "./utils/Layout.sol";
15
16
  import { Schemas } from "./blocks/Schema.sol";
16
17
  import { addrOr, applyBps, beforeBps, bytes32ToInt, bytes32ToString, divisible, hash32, intToBytes32, isFamily, matchesBase, MAX_BPS, max8, max16, max24, max32, max40, max64, max96, max128, max160, NotDivisible, retryTicket, toLocalBase, toUnspecifiedBase, ValueOverflow } from "./utils/Utils.sol";
@@ -72,36 +72,20 @@ library Cursors {
72
72
 
73
73
  /// @notice Create a cursor over `source` and restrict it to its first grouped run.
74
74
  /// Equivalent to `open(source)`, reading the current key, then `run(key, group)`.
75
+ /// When `group` is zero, `source` must be empty and the function returns an empty cursor.
75
76
  /// @param source Calldata slice that forms the block stream.
76
- /// @param group Expected block group size (e.g. 1 for single, 2 for paired).
77
+ /// @param group Expected block group size (e.g. 1 for single, 2 for paired); 0 means empty.
77
78
  /// @return cur Cursor with `len` truncated to the end of the first run in `source`.
78
79
  /// @return groups Number of block groups in the run (`block count / group`).
79
- /// @return next Byte offset immediately after the run, relative to `source`.
80
- function init(
81
- bytes calldata source,
82
- uint group
83
- ) internal pure returns (Cur memory cur, uint groups, uint next) {
80
+ function init(bytes calldata source, uint group) internal pure returns (Cur memory cur, uint groups) {
84
81
  cur = open(source);
85
- if (cur.i == cur.len) revert ZeroCursor();
82
+ if (group == 0) {
83
+ if (cur.len != 0) revert IncompleteCursor();
84
+ return (cur, 0);
85
+ }
86
+ if (cur.len == 0) revert ZeroCursor();
86
87
  (bytes4 key, ) = cur.peek(cur.i);
87
88
  groups = cur.run(key, group);
88
- next = cur.len;
89
- }
90
-
91
- /// @notice Create a cursor over `source`, restrict it to its first grouped run, and require an exact group count.
92
- /// @param source Calldata slice that forms the block stream.
93
- /// @param group Expected block group size (e.g. 1 for single, 2 for paired).
94
- /// @param expectedGroups Required number of groups in the run.
95
- /// @return cur Cursor with `len` truncated to the end of the first run in `source`.
96
- /// @return next Byte offset immediately after the run, relative to `source`.
97
- function init(
98
- bytes calldata source,
99
- uint group,
100
- uint expectedGroups
101
- ) internal pure returns (Cur memory cur, uint next) {
102
- uint groups;
103
- (cur, groups, next) = init(source, group);
104
- if (groups != expectedGroups) revert BadRatio();
105
89
  }
106
90
 
107
91
  /// @notice Move the cursor to an absolute position within the source region.
@@ -333,14 +317,15 @@ library Cursors {
333
317
  return find(cur, cur.i, key);
334
318
  }
335
319
 
336
- /// @notice Enter a List block at the current position and return the next offset.
337
- /// Advances `cur.i` past the list header so the list members can be parsed
338
- /// directly from the same cursor. The returned `next` is the byte offset
339
- /// immediately after the list payload, relative to the current cursor region.
340
- /// @param cur Cursor positioned at a list block; advanced past the 8-byte header.
341
- /// @return next Byte offset immediately after the list payload.
342
- function list(Cur memory cur) internal pure returns (uint next) {
343
- next = enter(cur, Keys.List, 0, 0);
320
+ /// @notice Consume a LIST block and return a cursor over its payload.
321
+ /// Advances `cur.i` past the full list while the returned cursor is scoped to
322
+ /// the list members as a fresh zero-based region.
323
+ /// @param cur Cursor positioned at a list block; advanced past the full list.
324
+ /// @return items Cursor scoped to the list payload.
325
+ function list(Cur memory cur) internal pure returns (Cur memory items) {
326
+ uint next = enter(cur, Keys.List, 0, 0);
327
+ items = cur.slice(cur.i, next);
328
+ cur.i = next;
344
329
  }
345
330
 
346
331
  /// @notice Consume a block with the given key at the current position and return a cursor over the full block slice.
@@ -378,22 +363,13 @@ library Cursors {
378
363
  return cur.isAt(key) ? take(cur, key) : cur.slice(cur.i, cur.i);
379
364
  }
380
365
 
381
- /// @notice Consume an optional DATA block at the current position and return a cursor over the full block slice.
382
- /// If the current block is not DATA, returns an empty cursor and leaves `cur.i` unchanged.
383
- /// Otherwise behaves like `take(cur, Keys.Data)`.
384
- /// @param cur Cursor positioned at an optional DATA block.
385
- /// @return out Cursor scoped to the full DATA block, or empty when no DATA block is present.
386
- function maybeData(Cur memory cur) internal pure returns (Cur memory out) {
387
- return maybeTake(cur, Keys.Data);
388
- }
389
-
390
- /// @notice Exit a nested region at an exact boundary.
391
- /// Reverts with `IncompleteCursor` if `end` exceeds the cursor region length
392
- /// or `cur.i != end`.
366
+ /// @notice Ensure the cursor is at an exact position.
367
+ /// Reverts with `IncompleteCursor` if `pos` exceeds the cursor region length
368
+ /// or `cur.i != pos`.
393
369
  /// @param cur Cursor to check.
394
- /// @param end Relative end offset of the nested region.
395
- function exit(Cur memory cur, uint end) internal pure {
396
- if (end > cur.len || cur.i != end) revert IncompleteCursor();
370
+ /// @param pos Relative byte offset the cursor must be positioned at.
371
+ function ensureAt(Cur memory cur, uint pos) internal pure {
372
+ if (pos > cur.len || cur.i != pos) revert IncompleteCursor();
397
373
  }
398
374
 
399
375
  /// @notice Assert that the cursor has consumed its entire source region.
@@ -747,7 +723,7 @@ library Cursors {
747
723
  id = cur.readUint();
748
724
  namespace = cur.read32();
749
725
  name = cur.unpackString();
750
- cur.exit(end);
726
+ cur.ensureAt(end);
751
727
  }
752
728
 
753
729
  /// @notice Consume a dynamic block with a single bytes32 payload.
@@ -825,10 +801,7 @@ library Cursors {
825
801
  /// @param key Expected block key.
826
802
  /// @return asset Asset identifier.
827
803
  /// @return amount Scalar amount value.
828
- function unpackAssetAmount(
829
- Cur memory cur,
830
- bytes4 key
831
- ) internal pure returns (bytes32 asset, uint amount) {
804
+ function unpackAssetAmount(Cur memory cur, bytes4 key) internal pure returns (bytes32 asset, uint amount) {
832
805
  uint abs = consume(cur, 0, key, 64, 64);
833
806
  asset = bytes32(msg.data[abs:abs + 32]);
834
807
  amount = uint(bytes32(msg.data[abs + 32:abs + 64]));
@@ -992,9 +965,7 @@ library Cursors {
992
965
  /// @return host Host node ID.
993
966
  /// @return account Account identifier.
994
967
  /// @return asset Asset identifier.
995
- function unpackHostAccountAsset(
996
- Cur memory cur
997
- ) internal pure returns (uint host, bytes32 account, bytes32 asset) {
968
+ function unpackHostAccountAsset(Cur memory cur) internal pure returns (uint host, bytes32 account, bytes32 asset) {
998
969
  return unpackHostAccountAsset(cur, Keys.HostAccountAsset);
999
970
  }
1000
971
 
@@ -1010,9 +981,7 @@ library Cursors {
1010
981
  /// @return account Account identifier.
1011
982
  /// @return asset Asset identifier.
1012
983
  /// @return amount Token amount.
1013
- function unpackAccountAmount(
1014
- Cur memory cur
1015
- ) internal pure returns (bytes32 account, bytes32 asset, uint amount) {
984
+ function unpackAccountAmount(Cur memory cur) internal pure returns (bytes32 account, bytes32 asset, uint amount) {
1016
985
  return unpackAccountAmount(cur, Keys.AccountAmount);
1017
986
  }
1018
987
 
@@ -1028,9 +997,7 @@ library Cursors {
1028
997
  /// @return host Host node ID.
1029
998
  /// @return asset Asset identifier.
1030
999
  /// @return amount Token amount.
1031
- function unpackAllocation(
1032
- Cur memory cur
1033
- ) internal pure returns (uint host, bytes32 asset, uint amount) {
1000
+ function unpackAllocation(Cur memory cur) internal pure returns (uint host, bytes32 asset, uint amount) {
1034
1001
  return unpackHostAmount(cur, Keys.Allocation);
1035
1002
  }
1036
1003
 
@@ -1046,9 +1013,7 @@ library Cursors {
1046
1013
  /// @return host Host node ID.
1047
1014
  /// @return asset Asset identifier.
1048
1015
  /// @return amount Token amount.
1049
- function unpackAllowance(
1050
- Cur memory cur
1051
- ) internal pure returns (uint host, bytes32 asset, uint amount) {
1016
+ function unpackAllowance(Cur memory cur) internal pure returns (uint host, bytes32 asset, uint amount) {
1052
1017
  return unpackHostAmount(cur, Keys.Allowance);
1053
1018
  }
1054
1019
 
@@ -1107,7 +1072,7 @@ library Cursors {
1107
1072
  target = uint(cur.read32());
1108
1073
  resources = uint(cur.read32());
1109
1074
  req = cur.unpackBytes();
1110
- cur.exit(end);
1075
+ cur.ensureAt(end);
1111
1076
  }
1112
1077
 
1113
1078
  /// @notice Consume a CALL block and return its target invocation fields.
@@ -1121,7 +1086,7 @@ library Cursors {
1121
1086
  target = uint(cur.read32());
1122
1087
  resources = uint(cur.read32());
1123
1088
  data = cur.unpackBytes();
1124
- cur.exit(end);
1089
+ cur.ensureAt(end);
1125
1090
  }
1126
1091
 
1127
1092
  /// @notice Consume a CONTEXT block and return its command context fields.
@@ -1137,7 +1102,7 @@ library Cursors {
1137
1102
  account = cur.read32();
1138
1103
  state = cur.unpackBytes();
1139
1104
  request = cur.unpackBytes();
1140
- cur.exit(end);
1105
+ cur.ensureAt(end);
1141
1106
  }
1142
1107
 
1143
1108
  /// @notice Consume a RELAY block and return its destination portal, resources, and request stream.
@@ -1145,14 +1110,12 @@ library Cursors {
1145
1110
  /// @return portal Destination portal identifier, often the destination host ID.
1146
1111
  /// @return resources Chain-specific resources for the destination context.
1147
1112
  /// @return request Embedded request block stream.
1148
- function unpackRelay(
1149
- Cur memory cur
1150
- ) internal pure returns (uint portal, uint resources, bytes calldata request) {
1113
+ function unpackRelay(Cur memory cur) internal pure returns (uint portal, uint resources, bytes calldata request) {
1151
1114
  uint end = cur.enter(Keys.Relay, 64 + Sizes.Header, 0);
1152
1115
  portal = cur.readUint();
1153
1116
  resources = cur.readUint();
1154
1117
  request = cur.unpackBytes();
1155
- cur.exit(end);
1118
+ cur.ensureAt(end);
1156
1119
  }
1157
1120
 
1158
1121
  /// @notice Consume a DISPATCH block and return its destination portal, resources, and payload.
@@ -1167,7 +1130,7 @@ library Cursors {
1167
1130
  portal = cur.readUint();
1168
1131
  resources = cur.readUint();
1169
1132
  payload = cur.unpackBytes();
1170
- cur.exit(end);
1133
+ cur.ensureAt(end);
1171
1134
  }
1172
1135
 
1173
1136
  /// @notice Consume a RECOVER block and return its handler, resources, key, and witness bytes.
@@ -1184,7 +1147,7 @@ library Cursors {
1184
1147
  resources = cur.readUint();
1185
1148
  key = cur.read32();
1186
1149
  witness = cur.unpackBytes();
1187
- cur.exit(end);
1150
+ cur.ensureAt(end);
1188
1151
  }
1189
1152
 
1190
1153
  // Type-specific validators
@@ -1214,11 +1177,7 @@ library Cursors {
1214
1177
  /// @param key Expected block type key.
1215
1178
  /// @param asset Expected asset identifier.
1216
1179
  /// @return amount Amount from the block.
1217
- function requireAssetAmount(
1218
- Cur memory cur,
1219
- bytes4 key,
1220
- bytes32 asset
1221
- ) internal pure returns (uint amount) {
1180
+ function requireAssetAmount(Cur memory cur, bytes4 key, bytes32 asset) internal pure returns (uint amount) {
1222
1181
  uint abs = consume(cur, 0, key, 64, 64);
1223
1182
  if (bytes32(msg.data[abs:abs + 32]) != asset) revert UnexpectedValue();
1224
1183
  amount = uint(bytes32(msg.data[abs + 32:abs + 64]));
@@ -1274,12 +1233,7 @@ library Cursors {
1274
1233
  /// @param key Expected block type key.
1275
1234
  /// @param host Expected host node ID.
1276
1235
  /// @param asset Expected asset identifier.
1277
- function requireUnitHostAmount(
1278
- Cur memory cur,
1279
- bytes4 key,
1280
- uint host,
1281
- bytes32 asset
1282
- ) internal pure {
1236
+ function requireUnitHostAmount(Cur memory cur, bytes4 key, uint host, bytes32 asset) internal pure {
1283
1237
  uint abs = consume(cur, 0, key, 96, 96);
1284
1238
  if (uint(bytes32(msg.data[abs:abs + 32])) != host) revert UnexpectedValue();
1285
1239
  if (bytes32(msg.data[abs + 32:abs + 64]) != asset) revert UnexpectedValue();
@@ -1326,10 +1280,7 @@ library Cursors {
1326
1280
  /// @param host Expected host node ID.
1327
1281
  /// @return account Account identifier from the block.
1328
1282
  /// @return asset Asset identifier from the block.
1329
- function requireHostAccountAsset(
1330
- Cur memory cur,
1331
- uint host
1332
- ) internal pure returns (bytes32 account, bytes32 asset) {
1283
+ function requireHostAccountAsset(Cur memory cur, uint host) internal pure returns (bytes32 account, bytes32 asset) {
1333
1284
  return requireHostAccountAsset(cur, Keys.HostAccountAsset, host);
1334
1285
  }
1335
1286
 
package/blocks/Keys.sol CHANGED
@@ -2,9 +2,21 @@
2
2
  pragma solidity ^0.8.33;
3
3
 
4
4
  /// @title Keys
5
- /// @notice Block type selectors for the rootzero block stream protocol.
6
- /// Each key is the first 4 bytes of the keccak256 hash of its block name.
5
+ /// @notice Standard block type selectors for the rootzero block stream protocol.
6
+ /// Standard keys use the first 4 bytes of `keccak256("#name")` by convention.
7
+ /// Custom block keys only need to be unique in the context where they are used;
8
+ /// hosts may publish custom key meanings with the `Schema` event.
7
9
  library Keys {
10
+ /// @notice Create a context-local block key.
11
+ /// @dev Local keys are opaque tags for host- or endpoint-specific schemas.
12
+ /// The caller is responsible for choosing values that are unique in the
13
+ /// context where they are used and publishing their meaning with `Schema`.
14
+ /// @param value Opaque local key value.
15
+ /// @return Context-local block key.
16
+ function local(uint32 value) internal pure returns (bytes4) {
17
+ return bytes4(value);
18
+ }
19
+
8
20
  /// @dev Empty / unset key.
9
21
  bytes4 constant Empty = bytes4(0);
10
22
  /// @dev Wildcard key used in discovery when any block stream is accepted.
@@ -27,8 +39,6 @@ library Keys {
27
39
  bytes4 constant Fee = bytes4(keccak256("#fee"));
28
40
  /// @dev List wrapper; payload is an embedded repeated block stream
29
41
  bytes4 constant List = bytes4(keccak256("#list"));
30
- /// @dev Extensible data field; layout is schema-defined
31
- bytes4 constant Data = bytes4(keccak256("#data"));
32
42
  /// @dev EVM-encoded payload field; layout follows standard ABI tuple encoding
33
43
  bytes4 constant Evm = bytes4(keccak256("#evm"));
34
44
  /// @dev Reserved raw bytes child block.