@rootzero/contracts 1.10.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 (58) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/Core.sol +1 -0
  3. package/Endpoints.sol +1 -3
  4. package/Events.sol +2 -5
  5. package/README.md +35 -33
  6. package/Utils.sol +2 -1
  7. package/blocks/Cursors.sol +26 -75
  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 +7 -9
  18. package/commands/Relay.sol +10 -11
  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/docs/Schema.md +114 -82
  32. package/events/Endpoint.sol +19 -0
  33. package/events/Schema.sol +23 -0
  34. package/guards/Base.sol +17 -8
  35. package/guards/Revoke.sol +4 -6
  36. package/package.json +1 -1
  37. package/ports/AllowAssets.sol +6 -9
  38. package/ports/Allowance.sol +6 -9
  39. package/ports/Base.sol +21 -8
  40. package/ports/Credit.sol +6 -9
  41. package/ports/Debit.sol +6 -9
  42. package/ports/DenyAssets.sol +6 -9
  43. package/ports/Dispatch.sol +6 -9
  44. package/ports/Pipe.sol +4 -7
  45. package/ports/Redeem.sol +4 -7
  46. package/ports/Settle.sol +6 -9
  47. package/queries/Assets.sol +9 -12
  48. package/queries/Balances.sol +7 -9
  49. package/queries/Base.sol +19 -11
  50. package/utils/Selectors.sol +49 -0
  51. package/commands/admin/Destroy.sol +0 -43
  52. package/commands/admin/Init.sol +0 -43
  53. package/events/Admin.sol +0 -32
  54. package/events/Command.sol +0 -32
  55. package/events/Guard.sol +0 -18
  56. package/events/Port.sol +0 -22
  57. package/events/Query.sol +0 -20
  58. package/queries/Positions.sol +0 -54
package/CHANGELOG.md CHANGED
@@ -3,7 +3,26 @@
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
- ## Unreleased
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.
7
26
 
8
27
  ## 1.10.0
9
28
 
package/Core.sol CHANGED
@@ -10,6 +10,7 @@ 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
16
  import { Portal } from "./core/Portal.sol";
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
@@ -26,11 +27,9 @@ import { AllowAssets, AllowAssetsHook } from "./commands/admin/AllowAssets.sol";
26
27
  import { Allowance, AllowanceHook } from "./commands/admin/Allowance.sol";
27
28
  import { Appoint } from "./commands/admin/Appoint.sol";
28
29
  import { Authorize } from "./commands/admin/Authorize.sol";
29
- import { Destroy, DestroyHook } from "./commands/admin/Destroy.sol";
30
30
  import { DenyAssets, DenyAssetsHook } from "./commands/admin/DenyAssets.sol";
31
31
  import { Dismiss } from "./commands/admin/Dismiss.sol";
32
32
  import { ExecutePayable } from "./commands/admin/Execute.sol";
33
- import { Init, InitHook } from "./commands/admin/Init.sol";
34
33
  import { Label } from "./commands/admin/Label.sol";
35
34
  import { Unauthorize } from "./commands/admin/Unauthorize.sol";
36
35
 
@@ -54,4 +53,3 @@ import { Revoke } from "./guards/Revoke.sol";
54
53
  import { QueryBase } from "./queries/Base.sol";
55
54
  import { AssetStatus, AssetStatusHook } from "./queries/Assets.sol";
56
55
  import { GetBalances, GetBalancesHook } from "./queries/Balances.sol";
57
- 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
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,35 +72,22 @@ 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
- function init(
80
- bytes calldata source,
81
- uint group
82
- ) internal pure returns (Cur memory cur, uint groups) {
80
+ function init(bytes calldata source, uint group) internal pure returns (Cur memory cur, uint groups) {
83
81
  cur = open(source);
84
- 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();
85
87
  (bytes4 key, ) = cur.peek(cur.i);
86
88
  groups = cur.run(key, group);
87
89
  }
88
90
 
89
- /// @notice Create a cursor over `source`, restrict it to its first grouped run, and require an exact group count.
90
- /// @param source Calldata slice that forms the block stream.
91
- /// @param group Expected block group size (e.g. 1 for single, 2 for paired).
92
- /// @param expectedGroups Required number of groups in the run.
93
- /// @return cur Cursor with `len` truncated to the end of the first run in `source`.
94
- function init(
95
- bytes calldata source,
96
- uint group,
97
- uint expectedGroups
98
- ) internal pure returns (Cur memory cur) {
99
- uint groups;
100
- (cur, groups) = init(source, group);
101
- if (groups != expectedGroups) revert BadRatio();
102
- }
103
-
104
91
  /// @notice Move the cursor to an absolute position within the source region.
105
92
  /// @param cur Cursor to update.
106
93
  /// @param i New read position (byte offset relative to source start).
@@ -330,17 +317,15 @@ library Cursors {
330
317
  return find(cur, cur.i, key);
331
318
  }
332
319
 
333
- /// @notice Enter a LIST block at the expected current position and return its next offset.
334
- /// Reverts with `IncompleteCursor` if `cur.i` is not exactly `pos`.
335
- /// Advances `cur.i` past the list header so the list members can be parsed
336
- /// directly from the same cursor. The returned `next` is the byte offset
337
- /// immediately after the list payload, relative to the current cursor region.
338
- /// @param cur Cursor expected to be positioned at a list block; advanced past the 8-byte header.
339
- /// @param pos Expected current cursor position, relative to the cursor region.
340
- /// @return next Byte offset immediately after the list payload.
341
- function list(Cur memory cur, uint pos) internal pure returns (uint next) {
342
- cur.ensureAt(pos);
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,15 +363,6 @@ 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
366
  /// @notice Ensure the cursor is at an exact position.
391
367
  /// Reverts with `IncompleteCursor` if `pos` exceeds the cursor region length
392
368
  /// or `cur.i != pos`.
@@ -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
 
@@ -1145,9 +1110,7 @@ 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();
@@ -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.
package/blocks/Schema.sol CHANGED
@@ -7,31 +7,36 @@ pragma solidity ^0.8.33;
7
7
  // - payload layout is block-specific
8
8
  //
9
9
  // Schema:
10
- // - blocks are written as `#name { fields }`
11
- // - a block without braces has no payload, e.g. `#unit`
10
+ // - block aliases are published separately from payload schemas
11
+ // - payload schemas are written as `{ fields }`
12
+ // - an empty schema string means the block has no structured payload
12
13
  // - commas separate siblings at every level
13
- // - braces define parent-child boundaries
14
+ // - braces define the current block payload body
14
15
  // - command requests are a single run when the request schema is non-empty
15
16
  // - command state is a single active state run without trailing globals
16
17
  // - run items may repeat at top level for batching
17
- // - `maybe #x { ... }` marks an optional block item
18
- // - `many #x { ... }` emits one generic list block containing repeated `#x` items
18
+ // - `maybe #x` marks an optional block item
19
+ // - `many #x` emits one generic list block containing repeated `#x` items
20
+ // - endpoint descriptor lanes are `[key bytes4][item bytes4]`; normal keys widen to `[key][0]`
21
+ // - descriptor lanes for `many #x` use `[Keys.List][keyOfX]`; bare `[Keys.List][0]`
22
+ // is incomplete discovery metadata and should be rejected by tooling
19
23
  // - `portal` fields are routing identifiers, often destination host IDs
20
24
  // - `resources` fields are chain-specific resource words. A portal adapter
21
25
  // interprets them for the destination runtime. EVM resources use the low
22
26
  // 128 bits as native value.
23
27
  // - dotted field names and aliases, e.g. `dst.portal` or `#bytes as dst.payload`,
24
28
  // are offchain projection metadata only and do not change runtime encoding
25
- // - a child block without an inline body, e.g. `#context as witness`, may resolve
26
- // to a known schema in the active schema context; unresolved aliases are invalid
29
+ // - child blocks resolve by alias in the active schema context; unresolved aliases are invalid
30
+ // - schema strings describe the payload body only; the `Block` event carries the alias
27
31
  // - fixed fields are packed in declaration order
28
32
  // - blocks have fixed fields followed by a dynamic child-block tail
29
33
  // - child block tails are embedded directly, without an extra stream wrapper
30
34
  // - `#bytes` is a reserved child block that stores raw bytes and has no body
31
35
  // - `#string` is a reserved child block that stores UTF-8 string bytes and has no body
32
- // - generic `#data` uses the stable key derived from `#data`
33
36
  // - generic lists use the stable key derived from `#list`
34
- // - keys are derived from block names, e.g. bytes4(keccak256("#amount"))
37
+ // - standard keys are derived from block aliases, e.g. bytes4(keccak256("#amount"))
38
+ // - custom keys are opaque bytes4 tags and only need to be unique in their
39
+ // active context; use `Schema(host, key, schema, name)` to publish their meaning
35
40
  // - see `docs/Schema.md` for the full working spec
36
41
  //
37
42
  // Pipeline state:
@@ -57,48 +62,48 @@ pragma solidity ^0.8.33;
57
62
  /// @title Schemas
58
63
  /// @notice Human-readable schema string constants for each block type.
59
64
  /// These strings describe payload layout for discovery events and docs; block
60
- /// keys are derived only from block names.
65
+ /// aliases map to standard keys by convention. Custom blocks may use any unique
66
+ /// bytes4 key in their active context.
61
67
  library Schemas {
62
- string constant Unit = "#unit";
63
- string constant Node = "#node { uint id }";
64
- string constant Account = "#account { bytes32 account }";
65
- string constant Asset = "#asset { bytes32 asset }";
66
- string constant Amount = "#amount { bytes32 asset, uint amount }";
67
- string constant Balance = "#balance { bytes32 asset, uint amount }";
68
- string constant BalanceLimit = "#balanceLimit { bytes32 asset, uint min, uint max }";
69
- string constant Custody = "#custody { uint host, bytes32 asset, uint amount }";
70
- string constant CustodyLimit = "#custodyLimit { uint host, bytes32 asset, uint min, uint max }";
71
- string constant Allocation = "#allocation { uint host, bytes32 asset, uint amount }";
72
- string constant Allowance = "#allowance { uint host, bytes32 asset, uint amount }";
73
- string constant Transaction = "#transaction { bytes32 from, bytes32 to, bytes32 asset, uint amount }";
74
- string constant Context = "#context { bytes32 account, #bytes as state, #bytes as request }";
75
- string constant Recover = "#recover { uint handler, uint resources, bytes32 key, #bytes as witness }";
76
- string constant Call = "#call { uint target, uint resources, #bytes as payload }";
77
- string constant Step = "#step { uint target, uint resources, #bytes as request }";
78
- string constant Relay = "#relay { uint portal, uint resources, #bytes as request }";
79
- string constant Dispatch = "#dispatch { uint portal, uint resources, #bytes as payload }";
80
- string constant Bounty = "#bounty { uint amount, bytes32 relayer }";
81
- string constant Fee = "#fee { uint amount }";
82
- string constant Auth = "#auth { uint cid, uint deadline, #bytes as proof }";
83
- string constant Label = "#label { uint id, bytes32 namespace, #string as name }";
84
- string constant Bytes = "#bytes";
85
- string constant String = "#string";
86
- string constant Data = "#data";
87
- string constant List = "#list";
88
- string constant Evm = "#evm";
68
+ string constant Unit = "";
69
+ string constant Node = "{ uint id }";
70
+ string constant Account = "{ bytes32 account }";
71
+ string constant Asset = "{ bytes32 asset }";
72
+ string constant Amount = "{ bytes32 asset, uint amount }";
73
+ string constant Balance = "{ bytes32 asset, uint amount }";
74
+ string constant BalanceLimit = "{ bytes32 asset, uint min, uint max }";
75
+ string constant Custody = "{ uint host, bytes32 asset, uint amount }";
76
+ string constant CustodyLimit = "{ uint host, bytes32 asset, uint min, uint max }";
77
+ string constant Allocation = "{ uint host, bytes32 asset, uint amount }";
78
+ string constant Allowance = "{ uint host, bytes32 asset, uint amount }";
79
+ string constant Transaction = "{ bytes32 from, bytes32 to, bytes32 asset, uint amount }";
80
+ string constant Context = "{ bytes32 account, #bytes as state, #bytes as request }";
81
+ string constant Recover = "{ uint handler, uint resources, bytes32 key, #bytes as witness }";
82
+ string constant Call = "{ uint target, uint resources, #bytes as payload }";
83
+ string constant Step = "{ uint target, uint resources, #bytes as request }";
84
+ string constant Relay = "{ uint portal, uint resources, #bytes as request }";
85
+ string constant Dispatch = "{ uint portal, uint resources, #bytes as payload }";
86
+ string constant Bounty = "{ uint amount, bytes32 relayer }";
87
+ string constant Fee = "{ uint amount }";
88
+ string constant Auth = "{ uint cid, uint deadline, #bytes as proof }";
89
+ string constant Label = "{ uint id, bytes32 namespace, #string as name }";
90
+ string constant Bytes = "";
91
+ string constant String = "";
92
+ string constant List = "";
93
+ string constant Evm = "";
89
94
  }
90
95
 
91
96
  /// @title Forms
92
97
  /// @notice Reusable structural block schemas for core tuple shapes.
93
98
  /// These describe payload form without assigning command or query semantics.
94
99
  library Forms {
95
- string constant Status = "#status { uint code }";
96
- string constant AssetAmount = "#assetAmount { bytes32 asset, uint amount }";
97
- string constant AccountAsset = "#accountAsset { bytes32 account, bytes32 asset }";
98
- string constant AccountAmount = "#accountAmount { bytes32 account, bytes32 asset, uint amount }";
99
- string constant HostAmount = "#hostAmount { uint host, bytes32 asset, uint amount }";
100
- string constant HostAccountAsset = "#hostAccountAsset { uint host, bytes32 account, bytes32 asset }";
101
- string constant HostAccountAmount = "#hostAccountAmount { uint host, bytes32 account, bytes32 asset, uint amount }";
100
+ string constant Status = "{ uint code }";
101
+ string constant AssetAmount = "{ bytes32 asset, uint amount }";
102
+ string constant AccountAsset = "{ bytes32 account, bytes32 asset }";
103
+ string constant AccountAmount = "{ bytes32 account, bytes32 asset, uint amount }";
104
+ string constant HostAmount = "{ uint host, bytes32 asset, uint amount }";
105
+ string constant HostAccountAsset = "{ uint host, bytes32 account, bytes32 asset }";
106
+ string constant HostAccountAmount = "{ uint host, bytes32 account, bytes32 asset, uint amount }";
102
107
  }
103
108
 
104
109
  /// @title Sizes