@rootzero/contracts 1.2.0 → 1.4.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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/Endpoints.sol +9 -7
  3. package/Events.sol +2 -2
  4. package/README.md +300 -79
  5. package/blocks/Cursors.sol +10 -14
  6. package/blocks/Schema.sol +7 -4
  7. package/commands/Burn.sol +2 -2
  8. package/commands/Credit.sol +2 -2
  9. package/commands/Debit.sol +2 -2
  10. package/commands/Deposit.sol +4 -4
  11. package/commands/Payout.sol +3 -3
  12. package/commands/Provision.sol +4 -4
  13. package/commands/Relay.sol +2 -2
  14. package/commands/Withdraw.sol +2 -2
  15. package/commands/admin/AllowAssets.sol +2 -2
  16. package/commands/admin/Allowance.sol +2 -2
  17. package/commands/admin/Appoint.sol +2 -2
  18. package/commands/admin/Authorize.sol +2 -2
  19. package/commands/admin/DenyAssets.sol +2 -2
  20. package/commands/admin/Destroy.sol +1 -1
  21. package/commands/admin/Dismiss.sol +2 -2
  22. package/commands/admin/Execute.sol +2 -2
  23. package/commands/admin/Init.sol +1 -1
  24. package/commands/admin/Label.sol +2 -2
  25. package/commands/admin/Unauthorize.sol +2 -2
  26. package/core/Pipeline.sol +1 -1
  27. package/docs/Schema.md +59 -2
  28. package/events/Admin.sol +2 -6
  29. package/events/Command.sol +2 -6
  30. package/events/Commander.sol +19 -0
  31. package/events/Labeled.sol +7 -4
  32. package/events/Route.sol +18 -0
  33. package/guards/Revoke.sol +1 -1
  34. package/package.json +1 -1
  35. package/peer/AllowAssets.sol +6 -2
  36. package/peer/Allowance.sol +6 -2
  37. package/peer/BalancePull.sol +6 -2
  38. package/peer/Credit.sol +39 -0
  39. package/peer/Debit.sol +39 -0
  40. package/peer/DenyAssets.sol +6 -2
  41. package/peer/Dispatch.sol +6 -2
  42. package/peer/Pipe.sol +6 -2
  43. package/peer/Settle.sol +6 -2
  44. package/queries/Assets.sol +1 -1
  45. package/queries/Balances.sol +1 -1
  46. package/queries/Positions.sol +1 -1
  47. package/utils/Actions.sol +1 -0
  48. package/events/Chain.sol +0 -19
  49. package/events/Transfer.sol +0 -22
package/CHANGELOG.md CHANGED
@@ -3,6 +3,32 @@
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.4.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - Replaced the `Chain` discovery event with `Commander(uint indexed host, uint chain, bytes32 native, bytes32 admin)`.
11
+ - Removed the `Transfer` flow event; account flows should be represented with `Spent` and `Received`.
12
+ - Renamed the indexed `Labeled` event parameter from `id` to `entity` in the published ABI string.
13
+
14
+ ### Added
15
+
16
+ - Added `Route(uint indexed host, uint chain, uint context)` for generic cross-chain route discovery.
17
+ - Added `Actions.Refund` for unused payable command value returned through settlement hooks.
18
+
19
+ ## 1.3.0
20
+
21
+ ### Breaking Changes
22
+
23
+ - Simplified `Cursors.init` to parse a single run from the start of a calldata slice. Callers that previously passed an offset must slice first or use `Cursors.open(source, i)`.
24
+ - Tightened command, query, and peer request parsing around the single-run convention used by current protocol endpoints.
25
+
26
+ ### Added
27
+
28
+ - Added `peerCreditTo` and `peerDebitFrom` peer endpoints for account-scoped `ACCOUNT_AMOUNT` batches.
29
+ - Added same-file `IPeer*` interfaces for every peer endpoint and exported them from `Endpoints.sol`.
30
+ - Added indexing documentation covering discovery, labels, access state, balance events, and flow-event conventions.
31
+
6
32
  ## 1.2.0
7
33
 
8
34
  ### Breaking Changes
package/Endpoints.sol CHANGED
@@ -34,13 +34,15 @@ import { Unauthorize } from "./commands/admin/Unauthorize.sol";
34
34
 
35
35
  // Peer endpoints
36
36
  import { PeerBase, encodePeerCall } from "./peer/Base.sol";
37
- import { PeerAllowAssets } from "./peer/AllowAssets.sol";
38
- import { PeerAllowance } from "./peer/Allowance.sol";
39
- import { PeerBalancePull, BalancePullHook } from "./peer/BalancePull.sol";
40
- import { PeerDenyAssets } from "./peer/DenyAssets.sol";
41
- import { PeerPipePayable } from "./peer/Pipe.sol";
42
- import { PeerDispatchPayable } from "./peer/Dispatch.sol";
43
- import { PeerSettle } from "./peer/Settle.sol";
37
+ import { PeerAllowAssets, IPeerAllowAssets } from "./peer/AllowAssets.sol";
38
+ import { PeerAllowance, IPeerAllowance } from "./peer/Allowance.sol";
39
+ import { PeerBalancePull, BalancePullHook, IPeerBalancePull } from "./peer/BalancePull.sol";
40
+ import { PeerCreditTo, IPeerCreditTo } from "./peer/Credit.sol";
41
+ import { PeerDebitFrom, IPeerDebitFrom } from "./peer/Debit.sol";
42
+ import { PeerDenyAssets, IPeerDenyAssets } from "./peer/DenyAssets.sol";
43
+ import { PeerPipePayable, IPeerPipePayable } from "./peer/Pipe.sol";
44
+ import { PeerDispatchPayable, IPeerDispatchPayable } from "./peer/Dispatch.sol";
45
+ import { PeerSettle, IPeerSettle } from "./peer/Settle.sol";
44
46
 
45
47
  // Guard endpoints
46
48
  import { GuardBase, encodeGuardCall } from "./guards/Base.sol";
package/Events.sol CHANGED
@@ -8,7 +8,7 @@ import { AdminEvent } from "./events/Admin.sol";
8
8
  import { AssetStatusEvent } from "./events/Asset.sol";
9
9
  import { Actions } from "./utils/Actions.sol";
10
10
  import { BalanceEvent } from "./events/Balance.sol";
11
- import { ChainEvent } from "./events/Chain.sol";
11
+ import { CommanderEvent } from "./events/Commander.sol";
12
12
  import { CommandEvent } from "./events/Command.sol";
13
13
  import { PositionEvent } from "./events/Position.sol";
14
14
  import { ReceivedEvent } from "./events/Received.sol";
@@ -22,8 +22,8 @@ import { NodeEvent } from "./events/Node.sol";
22
22
  import { PeerEvent } from "./events/Peer.sol";
23
23
  import { QueryEvent } from "./events/Query.sol";
24
24
  import { RootedEvent } from "./events/Rooted.sol";
25
+ import { RouteEvent } from "./events/Route.sol";
25
26
  import { SpentEvent } from "./events/Spent.sol";
26
- import { TransferEvent } from "./events/Transfer.sol";
27
27
  import { UnlockedEvent } from "./events/Unlocked.sol";
28
28
 
29
29
 
package/README.md CHANGED
@@ -1,128 +1,349 @@
1
1
  # rootzero
2
2
 
3
- `rootzero` is the Solidity library for building hosts and commands for the rootzero protocol.
3
+ rootzero is a protocol for building **hosts**: contracts that expose a uniform
4
+ set of endpoints over accounts and assets — commands that change state,
5
+ queries that read it, and peer links that connect hosts to each other, on the
6
+ same chain or across chains.
4
7
 
5
- It contains the reusable contracts, utilities, cursor parsers, and encoding helpers that rootzero applications compose on top of. If you are building a host, a command contract, or protocol tooling that needs to speak the protocol's ID, asset, account, and block formats, this repo is the shared foundation.
8
+ This repository is `@rootzero/contracts`, the Solidity library for the EVM port
9
+ of the protocol: the base contracts, block codecs, and helpers that rootzero
10
+ applications compose.
6
11
 
7
- ## Main Entry Points
12
+ Two decisions shape everything below. First, all data that crosses a host
13
+ boundary is encoded in one binary block format, so a request means the same
14
+ bytes on every chain. Second, every surface operates on *runs* of blocks rather
15
+ than single values, so batching is the default, not a feature added later. This
16
+ guide introduces the protocol bottom-up: blocks, then identities, then hosts
17
+ and the endpoints built on top of them.
8
18
 
9
- Most consumers should start from the package root entry points:
19
+ ## Quick Start
10
20
 
11
- - `@rootzero/contracts/Core.sol` - host, access control, balances, and validator building blocks
12
- - `@rootzero/contracts/Endpoints.sol` - command, peer, guard, and query base contracts plus standard endpoint mixins
13
- - `@rootzero/contracts/Cursors.sol` - cursor reader (`Cur`), block schemas, key constants, typed block helpers, and writers
14
- - `@rootzero/contracts/Utils.sol` - IDs, assets, accounts, layout, and value helpers
15
- - `@rootzero/contracts/Events.sol` - reusable event emitters and event contracts
21
+ Scaffold a ready-to-run Hardhat project, or add the library to an existing
22
+ one:
16
23
 
17
- ## Block Wire Format
24
+ ```bash
25
+ npx create-rootzero@latest my-app
26
+ # or
27
+ npm install @rootzero/contracts
28
+ ```
29
+
30
+ A minimal host composes the base `Host` with the endpoints it needs and
31
+ implements their policy hooks:
18
32
 
19
- All request and response data is encoded as a binary block stream. Each block is:
33
+ ```solidity
34
+ // SPDX-License-Identifier: GPL-3.0-only
35
+ pragma solidity ^0.8.33;
20
36
 
37
+ import { Host, Balances } from "@rootzero/contracts/Core.sol";
38
+ import { Deposit } from "@rootzero/contracts/Endpoints.sol";
39
+ import { Assets } from "@rootzero/contracts/Utils.sol";
40
+
41
+ contract ExampleHost is Host, Balances, Deposit {
42
+ constructor(address rootzero) Host(rootzero) {}
43
+
44
+ function deposit(bytes32 account, bytes32 asset, bytes32 meta, uint amount) internal override {
45
+ uint balance = creditTo(account, Assets.slot(asset, meta), amount);
46
+ emit Balance(account, asset, meta, balance, int(amount), depositId);
47
+ }
48
+ }
21
49
  ```
22
- [bytes4 key][bytes4 payloadLen][payload]
50
+
51
+ Deploy it with your own address as commander and you can call its commands
52
+ directly. A request is a run of binary blocks — here, a single `#amount` block
53
+ asking to deposit an asset (the encoders are a few lines each; see
54
+ [`test/helpers/blocks.ts`](test/helpers/blocks.ts) for reference
55
+ implementations):
56
+
57
+ ```ts
58
+ const host = await ethers.deployContract("ExampleHost", [deployer.address]);
59
+
60
+ const account = encodeUserAccount(user.address); // receiving account
61
+ const request = encodeAmountBlock(asset, meta, 100n); // what to deposit
62
+ await host.deposit({ account, state: "0x", request }); // emits Balance
23
63
  ```
24
64
 
25
- `key` is `bytes4(keccak256("#name"))`; see `Keys` for the full set and [`docs/Schema.md`](docs/Schema.md) for the schema format. `Cursors` parses calldata streams zero-copy via the `Cur` struct; `Writers` builds response streams into pre-allocated memory.
65
+ The rest of this guide explains the ideas this example leans on blocks, IDs,
66
+ hosts, commands — and the surfaces built on top of them.
26
67
 
27
- ## Schemas, Forms, And State
68
+ ## Blocks
28
69
 
29
- Protocol blocks use schema strings and four-byte keys:
70
+ Every request, response, and piece of in-flight state is a stream of typed
71
+ blocks. A block is a four-byte key, a four-byte big-endian length, and a
72
+ payload:
30
73
 
31
- - `Schemas` describes semantic protocol blocks such as `#amount`, `#balance`, `#custody`, and `#relay`.
32
- - `Forms` describes reusable structural blocks such as `#accountAsset` and `#accountAmount`, mostly used by queries.
33
- - `Keys` contains the runtime `bytes4` keys derived from block names.
74
+ ```txt
75
+ [bytes4 key][uint32 payloadLen][payload]
76
+ ```
34
77
 
35
- Every command declares its input and output pipeline state with block keys in the `Command` event. Use `Keys.Empty` when a command expects or returns no state.
78
+ The key is `bytes4(keccak256("#name"))`, and the payload layout is described by
79
+ a schema string. For example, the block that requests a deposit:
36
80
 
37
- The active command pipeline state is intentionally narrow: `BALANCE` and `CUSTODY` blocks are live value owned by the active account while execution is in-flight. `TRANSACTION` remains a block type for settlement messages, but it is not command pipeline state.
81
+ ```txt
82
+ #amount { bytes32 asset, bytes32 meta, uint amount }
83
+ ```
38
84
 
39
- ## Typical Usage
85
+ is 104 bytes on the wire: an 8-byte header followed by three big-endian 32-byte
86
+ fields. There is no ABI encoding and no chain-specific type anywhere in the
87
+ format — field types are chain-neutral integers, bytes, and booleans. A deposit
88
+ request built for an EVM host is byte-for-byte the request a CosmWasm or Solana
89
+ port would parse; what differs per chain is how a host *resolves* the
90
+ identifiers inside, never how the bytes are laid out.
91
+
92
+ Schemas can express more than flat fields: a block may end in nested child
93
+ blocks (`#bytes as payload` names a run of raw dynamic bytes), items can be
94
+ marked `maybe` (optional) or `many` (a list), and aliases and dotted field
95
+ paths give off-chain tooling presentation names without changing a single byte
96
+ on the wire. The full schema language is specified in
97
+ [`docs/Schema.md`](docs/Schema.md). The standard block schemas live in
98
+ `Schemas` and their runtime keys in `Keys` (both via
99
+ `@rootzero/contracts/Cursors.sol`).
100
+
101
+ ## Batches
102
+
103
+ A request is not a single struct; it is a run of blocks. One `#amount` block
104
+ asks for one deposit, five blocks ask for five, and the code path is identical
105
+ — every endpoint parses with a cursor and loops until the stream is exhausted.
106
+ The first item of a schema (the *prime item*) is the one that may repeat;
107
+ later top-level items, if any, apply to the whole batch.
108
+
109
+ Off-chain, building a batch is concatenation. Using the reference encoders from
110
+ [`test/helpers/blocks.ts`](test/helpers/blocks.ts):
111
+
112
+ ```ts
113
+ import { concat } from "ethers";
114
+ import { encodeAmountBlock } from "./helpers/blocks";
115
+
116
+ const request = concat([
117
+ encodeAmountBlock(usdc, meta, 250_000_000n),
118
+ encodeAmountBlock(dai, meta, 250n * 10n ** 18n),
119
+ ]);
120
+ // deposit(request) returns two #balance blocks, one per #amount
121
+ ```
40
122
 
41
- ### Build a Host
123
+ Everything downstream keeps this shape: commands loop over request blocks,
124
+ settlement loops over transactions, pipelines loop over steps. Batching is
125
+ never a special case.
42
126
 
43
- Extend `Host` when you want a rootzero host contract with admin command support and optional discovery registration.
127
+ ## IDs, Accounts, and Assets
44
128
 
45
- ```solidity
46
- // SPDX-License-Identifier: GPL-3.0-only
47
- pragma solidity ^0.8.33;
129
+ Everything the protocol touches — accounts, assets, chains, hosts, endpoints —
130
+ is identified by a self-describing 256-bit ID:
48
131
 
49
- import { Host } from "@rootzero/contracts/Core.sol";
132
+ ```txt
133
+ [uint32 type][uint32 chainid][192-bit payload]
134
+ ```
50
135
 
51
- contract ExampleHost is Host {
52
- constructor(address rootzero)
53
- Host(rootzero, 1, "example")
54
- {}
55
- }
136
+ where `type` packs `[vm][width][category][subtype]`. Any ID therefore announces
137
+ what it is (an account, an asset, a node) and which chain it lives on, and the
138
+ payload usually embeds the underlying address. User accounts are
139
+ chain-agnostic; admin and guardian accounts are chain-local. Assets cover the
140
+ native coin, ERC-20, ERC-721, and ERC-1155; wide identities carry a second
141
+ `meta` word (an ERC-721 token id, for example). Nodes are hosts, commands,
142
+ peers, queries, and guards.
143
+
144
+ The `Utils.sol` entry point provides the constructors and inspectors:
145
+
146
+ ```solidity
147
+ bytes32 account = Accounts.toUser(msg.sender); // chain-agnostic user account
148
+ bytes32 asset = Assets.toErc20(tokenAddress); // ERC-20 asset ID
149
+ uint hostId = Ids.toHost(address(this)); // host node ID
56
150
  ```
57
151
 
58
- `rootzero` is the trusted runtime address. If it implements `IHostDiscovery`, the host announces itself there during deployment. Use `address(0)` for a self-managed host that does not auto-register.
152
+ ## Hosts
59
153
 
60
- ### Build a Command
154
+ A host is one contract assembled from mixins. The base `Host` brings access
155
+ control and the admin surface (authorize, unauthorize, appoint, dismiss,
156
+ label, executePayable) plus the guardian `revoke` action; you add the
157
+ endpoints you need and the policy hooks they require. Keeping a ledger is
158
+ optional: the `Balances` mixin provides one, but a host can just as well
159
+ implement commands that hold no persistent state in the host at all —
160
+ forwarding funds elsewhere, or operating only on the state threaded through a
161
+ pipeline.
61
162
 
62
- Extend `CommandBase` to define a command mixin that runs inside the protocol's trusted call model. Commands are abstract contracts mixed into a host.
163
+ Trust is explicit and minimal. Each host has an immutable **commander**
164
+ address fixed at construction, from which its **admin account** is derived.
165
+ Other contracts become callers only when their node ID is authorized into the
166
+ host's trusted set, and **guardians** are accounts allowed to take protective
167
+ actions. At deployment, a host introduces itself to its commander, which is how
168
+ host topology becomes discoverable.
63
169
 
64
- ```solidity
65
- // SPDX-License-Identifier: GPL-3.0-only
66
- pragma solidity ^0.8.33;
170
+ The `ExampleHost` in the quick start shows the resulting split, and it runs
171
+ through the whole library: mixins implement the protocol mechanics (parsing,
172
+ batching, discovery events), and small virtual hooks let the host decide
173
+ policy — where funds come from, how the ledger is keyed, what gets emitted.
67
174
 
68
- import { CommandBase, CommandContext, Keys } from "@rootzero/contracts/Endpoints.sol";
69
- import { Cursors, Cur, Schemas, Writer, Writers } from "@rootzero/contracts/Cursors.sol";
175
+ ## Commands
70
176
 
71
- using Cursors for Cur;
72
- using Writers for Writer;
177
+ Commands are the write endpoints. Every command receives the same context:
73
178
 
74
- string constant NAME = "myCommand";
179
+ ```solidity
180
+ struct CommandContext {
181
+ bytes32 account; // acting account
182
+ bytes state; // block stream produced by the previous command
183
+ bytes request; // block stream for this invocation
184
+ }
185
+ ```
75
186
 
76
- abstract contract ExampleCommand is CommandBase {
77
- uint internal immutable myCommandId = commandId(NAME);
187
+ The request carries instructions; the state carries live value. While a
188
+ sequence of commands executes, `#balance` and `#custody` blocks in the state
189
+ are the funds being moved — produced by one command, consumed by the next.
78
190
 
79
- constructor() {
80
- emit Command(host, myCommandId, NAME, "1:0:1", Schemas.Amount, Keys.Empty, Keys.Balance, false, false);
191
+ The standard `Deposit` mixin shows the canonical shape — init a cursor, loop
192
+ the batch, call the hook, write the output run:
193
+
194
+ ```solidity
195
+ function deposit(CommandContext calldata c) external onlyCommand returns (bytes memory) {
196
+ (Cur memory request, uint groups, ) = Cursors.init(c.request, 1);
197
+ Writer memory writer = Writers.allocBalances(groups);
198
+
199
+ while (request.i < request.len) {
200
+ (bytes32 asset, bytes32 meta, uint amount) = request.unpackAmount();
201
+ deposit(c.account, asset, meta, amount); // host policy hook
202
+ writer.appendBalance(asset, meta, amount);
81
203
  }
82
204
 
83
- function myCommand(
84
- CommandContext calldata c
85
- ) external onlyCommand returns (bytes memory) {
86
- (Cur memory request, uint groups, ) = Cursors.init(c.request, 0, 1);
87
- Writer memory writer = Writers.allocBalances(groups);
205
+ request.complete();
206
+ return writer.finish();
207
+ }
208
+ ```
209
+
210
+ A command announces itself when the host is deployed. Its constructor emits a
211
+ discovery event carrying the request schema, the expected and produced state
212
+ block keys, and a shape string (`"1:0:1"` = one request block per operation, no
213
+ input state, one output block per operation), plus a human-readable label:
88
214
 
89
- while (request.i < request.len) {
90
- (bytes32 asset, bytes32 meta, uint amount) = request.unpackAmount();
91
- writer.appendBalance(asset, meta, amount);
92
- }
215
+ ```solidity
216
+ abstract contract MyCommand is CommandBase {
217
+ uint internal immutable myCommandId = commandId(this.myCommand.selector);
93
218
 
94
- request.complete();
95
- return writer.finish();
219
+ constructor() {
220
+ emit Command(host, myCommandId, "1:0:1", Schemas.Amount, Keys.Empty, Keys.Balance, false);
221
+ emit Labeled(myCommandId, bytes32(0), "myCommand");
222
+ }
223
+
224
+ function myCommand(CommandContext calldata c) external onlyCommand returns (bytes memory) {
225
+ // parse c.request, loop, return the output state run
96
226
  }
97
227
  }
98
228
  ```
99
229
 
100
- ## Repo Layout
230
+ The standard commands cover the common ledger movements: `deposit` and
231
+ `depositPayable` (external funds in), `withdraw` and `burn` (funds out),
232
+ `debitAccount` and `creditAccount` (internal movements), `payout` (deliver
233
+ state to other accounts), `provision` (allocate custody on another host), and
234
+ `relayPayable` (hand a pipeline to another chain).
101
235
 
102
- - `contracts/core` - host, access control, balances, operation base, and signature validation
103
- - `contracts/commands` - standard command building blocks and admin commands
104
- - `contracts/peer` - peer protocol surfaces for inter-host asset flows and asset allow/deny
105
- - `contracts/guards` - guard action surfaces for delegated protection flows
106
- - `contracts/queries` - read-only query endpoints for protocol state
107
- - `contracts/blocks` - block stream schema (`Schema`), cursor parsing (`Cursors`), and writers (`Writers`)
108
- - `contracts/utils` - shared encoding helpers: IDs, assets, accounts, layout, ECDSA
109
- - `contracts/events` - protocol event contracts and emitters
110
- - `docs` - introductory documentation
236
+ ## Pipelines
111
237
 
112
- ## Install and Compile
238
+ A single command is rarely the whole story. A pipeline is a run of `#step`
239
+ blocks executed in order within one transaction:
113
240
 
114
- ```bash
115
- npm install @rootzero/contracts
116
- npm run compile
241
+ ```txt
242
+ #step { uint target, uint resources, #bytes as request }
243
+ ```
244
+
245
+ Each step names a target command, the resources it may spend, and its request.
246
+ The state threads through: whatever one command returns becomes the input
247
+ state of the next, and the final state must be empty — value cannot be left
248
+ dangling at the end of a pipeline. This is the core of `Pipeline.pipe`:
249
+
250
+ ```solidity
251
+ while (input.i < input.len) {
252
+ (uint target, uint resources, bytes calldata request) = input.unpackStep();
253
+ state = dispatch(target, account, state, request, useValue(budget, resources));
254
+ }
255
+ if (state.length != 0) revert UnexpectedState();
117
256
  ```
118
257
 
119
- ## When To Use This Repo
258
+ A transfer, for instance, is a two-step pipeline: `debitAccount` turns an
259
+ `#amount` request into `#balance` state, and `payout` consumes that state
260
+ toward a recipient. Because a pipeline is just blocks, it is also the unit of
261
+ command batching — and `resources` is a chain-typed word (on EVM, the low 128
262
+ bits are native value in wei, drawn from a shared budget), so the same pipeline
263
+ bytes are meaningful to every port.
264
+
265
+ ## Queries
120
266
 
121
- Use `rootzero` if you want to:
267
+ Queries are the read endpoints: view functions that take a block-stream request
268
+ and return a block-stream response, with the same batch shape as commands. The
269
+ standard `getBalances` query takes a run of positions and answers each one in
270
+ order:
122
271
 
123
- - create a new rootzero host
124
- - implement a new rootzero command
125
- - reuse the protocol's block format and wire encoding
126
- - share protocol-level Solidity code across multiple rootzero applications
272
+ ```txt
273
+ request: #accountAsset { bytes32 account, bytes32 asset, bytes32 meta }
274
+ response: #accountAmount { bytes32 account, bytes32 asset, bytes32 meta, uint amount }
275
+ ```
127
276
 
128
- If you are looking for a full end-user app or deployment repo, this library is the lower-level protocol package rather than the full product surface.
277
+ Like commands, every query announces its request and response schemas at
278
+ deployment, so tooling knows how to call it without artifacts.
279
+
280
+ ## Peers
281
+
282
+ Peers are the host-to-host surfaces, callable only by trusted hosts. The two
283
+ central ones are batches all the way down:
284
+
285
+ - `peerSettle` consumes `#transaction { bytes32 from, bytes32 to, bytes32 asset,
286
+ bytes32 meta, uint amount }` blocks, debiting `from` and crediting `to` per
287
+ block — how two hosts record settlement between their ledgers.
288
+ - `peerPipePayable` consumes `#pipe` blocks, each carrying an account, an
289
+ initial state, and a run of steps — a complete pipeline delivered by another
290
+ host, executed locally with its own resource budget.
291
+
292
+ This is also the cross-chain mechanism. `relayPayable` (or `peerDispatchPayable`)
293
+ wraps a pipe and addresses it to a chain; a bridge adapter moves the **raw
294
+ bytes**; the destination host parses them with the same cursor rules and runs
295
+ the same pipeline loop. Nothing in the payload is EVM-specific — step targets
296
+ are destination-local node IDs, and only the adapter boundary (native
297
+ transfers, address resolution, signatures) is chain-specific. The parity rule
298
+ for ports is strict: every chain's implementation must parse the same input
299
+ bytes and produce the same output bytes for every endpoint.
300
+
301
+ ## Guards and Admin
302
+
303
+ Admin commands use the regular command shape but are gated to the host's admin
304
+ account: trust management (`authorize`, `unauthorize`), guardian management
305
+ (`appoint`, `dismiss`), naming (`label`), asset gating (`allowAssets`,
306
+ `denyAssets`, `allowance`), lifecycle (`init`, `destroy`), and raw calls
307
+ (`executePayable`). Guards go the other way: direct actions guardians can take
308
+ without any command context — the default is `revoke`, which lets a guardian
309
+ drop a trusted node immediately.
310
+
311
+ ## Events and Discovery
312
+
313
+ Hosts are self-describing. At deployment a host emits the ABI of every event it
314
+ uses (`EventAbi`), a discovery event per endpoint with its full schemas, and
315
+ labels for human-readable names. State changes then follow evented
316
+ conventions: `Balance` for every ledger change and flow events (`Transfer`,
317
+ `Received`, `Spent`) for value movement, each tagged with the endpoint that
318
+ caused it. An indexer can reconstruct the entire repository — endpoints,
319
+ names, access sets, balances — from logs alone, with no artifact files.
320
+
321
+ ## Using the Library
322
+
323
+ Import from the package entry points rather than deep paths:
324
+
325
+ - `@rootzero/contracts/Core.sol` — `Host`, access control, `Balances`,
326
+ `Pipeline`, validator
327
+ - `@rootzero/contracts/Endpoints.sol` — command, admin, peer, guard, and query
328
+ mixins and their hooks
329
+ - `@rootzero/contracts/Cursors.sol` — `Cur` cursor reader, `Writers`, `Schemas`,
330
+ `Keys`
331
+ - `@rootzero/contracts/Utils.sol` — `Ids`, `Assets`, `Accounts`, layout and
332
+ value helpers
333
+ - `@rootzero/contracts/Events.sol` — protocol event contracts
334
+
335
+ Repo layout:
336
+
337
+ - `contracts/core` — host, access control, balances, pipeline, validation
338
+ - `contracts/commands` — standard commands and admin commands
339
+ - `contracts/peer` — peer surfaces for inter-host and cross-chain flows
340
+ - `contracts/guards` — guardian direct actions
341
+ - `contracts/queries` — read-only query endpoints
342
+ - `contracts/blocks` — block schema, cursor parsing, writers
343
+ - `contracts/utils` — IDs, assets, accounts, layout, ECDSA
344
+ - `contracts/events` — event contracts and emitters
345
+ - `docs` — [`Schema.md`](docs/Schema.md) (wire format and schema DSL)
346
+
347
+ Use this library to create a new rootzero host, implement a command, or reuse
348
+ the protocol's block format in tooling. It is the shared protocol foundation,
349
+ not an end-user application.
@@ -70,41 +70,37 @@ library Cursors {
70
70
  return open(source[i:]);
71
71
  }
72
72
 
73
- /// @notice Create a cursor over `source[i:]` and restrict it to its first grouped run.
74
- /// Equivalent to `open(source, i)`, reading the current key, then `run(key, group)`.
75
- /// @param source Calldata slice that forms the parent block stream.
76
- /// @param i Start byte offset within `source`.
73
+ /// @notice Create a cursor over `source` and restrict it to its first grouped run.
74
+ /// Equivalent to `open(source)`, reading the current key, then `run(key, group)`.
75
+ /// @param source Calldata slice that forms the block stream.
77
76
  /// @param group Expected block group size (e.g. 1 for single, 2 for paired).
78
- /// @return cur Cursor with `len` truncated to the end of the first run in `source[i:]`.
77
+ /// @return cur Cursor with `len` truncated to the end of the first run in `source`.
79
78
  /// @return groups Number of block groups in the run (`block count / group`).
80
79
  /// @return next Byte offset immediately after the run, relative to `source`.
81
80
  function init(
82
81
  bytes calldata source,
83
- uint i,
84
82
  uint group
85
83
  ) internal pure returns (Cur memory cur, uint groups, uint next) {
86
- cur = open(source, i);
84
+ cur = open(source);
87
85
  if (cur.i == cur.len) revert ZeroCursor();
88
86
  (bytes4 key, ) = cur.peek(cur.i);
89
87
  groups = cur.run(key, group);
90
- next = i + cur.len;
88
+ next = cur.len;
91
89
  }
92
90
 
93
- /// @notice Create a cursor over `source[i:]`, restrict it to its first grouped run, and require an exact group count.
94
- /// @param source Calldata slice that forms the parent block stream.
95
- /// @param i Start byte offset within `source`.
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.
96
93
  /// @param group Expected block group size (e.g. 1 for single, 2 for paired).
97
94
  /// @param expectedGroups Required number of groups in the run.
98
- /// @return cur Cursor with `len` truncated to the end of the first run in `source[i:]`.
95
+ /// @return cur Cursor with `len` truncated to the end of the first run in `source`.
99
96
  /// @return next Byte offset immediately after the run, relative to `source`.
100
97
  function init(
101
98
  bytes calldata source,
102
- uint i,
103
99
  uint group,
104
100
  uint expectedGroups
105
101
  ) internal pure returns (Cur memory cur, uint next) {
106
102
  uint groups;
107
- (cur, groups, next) = init(source, i, group);
103
+ (cur, groups, next) = init(source, group);
108
104
  if (groups != expectedGroups) revert BadRatio();
109
105
  }
110
106
 
package/blocks/Schema.sol CHANGED
@@ -11,16 +11,16 @@ pragma solidity ^0.8.33;
11
11
  // - a block without braces has no payload, e.g. `#unit`
12
12
  // - commas separate siblings at every level
13
13
  // - braces define parent-child boundaries
14
- // - command requests start with the input run when the request schema is non-empty
15
- // - postcheck command requests include a constraint run after the input run; if the
16
- // request schema is empty, the constraint run starts the request
17
- // - command state starts with the active state run; trailing state globals may follow
14
+ // - command requests are a single run when the request schema is non-empty
15
+ // - command state is a single active state run without trailing globals
18
16
  // - run items may repeat at top level for batching
19
17
  // - `maybe #x { ... }` marks an optional block item
20
18
  // - `many #x { ... }` emits one generic list block containing repeated `#x` items
21
19
  // - `resources` fields are chain-specific resource words; one chain type may
22
20
  // pack them differently from another, but a given chain type must use one
23
21
  // stable format everywhere. EVM resources use the low 128 bits as native value.
22
+ // - dotted field names and aliases, e.g. `dst.chain` or `#bytes as dst.payload`,
23
+ // are offchain projection metadata only and do not change runtime encoding
24
24
  // - fixed fields are packed in declaration order
25
25
  // - blocks have fixed fields followed by a dynamic child-block tail
26
26
  // - child block tails are embedded directly, without an extra stream wrapper
@@ -32,6 +32,9 @@ pragma solidity ^0.8.33;
32
32
  // - see `docs/Schema.md` for the full working spec
33
33
  //
34
34
  // Pipeline state:
35
+ // - command request and state streams are each a single run of blocks under the
36
+ // current protocol convention; the block format may support other shapes in
37
+ // future protocol surfaces
35
38
  // - `balance(...)` and `custody(...)` are live, linear state in the active command pipeline
36
39
  // - pipeline state belongs to the active account while the pipeline is executing
37
40
  // - while a balance or custody is in-flight as pipeline state, it is not simultaneously persisted
package/commands/Burn.sol CHANGED
@@ -23,7 +23,7 @@ abstract contract Burn is CommandBase, BurnHook {
23
23
  uint internal immutable burnId = commandId(this.burn.selector);
24
24
 
25
25
  constructor() {
26
- emit Command(host, burnId, "0:1:0", "", Keys.Balance, Keys.Empty, false, false);
26
+ emit Command(host, burnId, "0:1:0", "", Keys.Balance, Keys.Empty, false);
27
27
  emit Labeled(burnId, bytes32(0), "burn");
28
28
  }
29
29
 
@@ -31,7 +31,7 @@ abstract contract Burn is CommandBase, BurnHook {
31
31
  /// @param c Command context; `c.state` must contain BALANCE blocks.
32
32
  /// @return Empty output state.
33
33
  function burn(CommandContext calldata c) external onlyCommand returns (bytes memory) {
34
- (Cur memory state, , ) = Cursors.init(c.state, 0, 1);
34
+ (Cur memory state, , ) = Cursors.init(c.state, 1);
35
35
 
36
36
  while (state.i < state.len) {
37
37
  (bytes32 asset, bytes32 meta, uint amount) = state.unpackBalance();
@@ -23,7 +23,7 @@ abstract contract CreditAccount is CommandBase, CreditAccountHook {
23
23
  uint internal immutable creditAccountId = commandId(this.creditAccount.selector);
24
24
 
25
25
  constructor() {
26
- emit Command(host, creditAccountId, "0:1:0", "", Keys.Balance, Keys.Empty, false, false);
26
+ emit Command(host, creditAccountId, "0:1:0", "", Keys.Balance, Keys.Empty, false);
27
27
  emit Labeled(creditAccountId, bytes32(0), "creditAccount");
28
28
  }
29
29
 
@@ -33,7 +33,7 @@ abstract contract CreditAccount is CommandBase, CreditAccountHook {
33
33
  function creditAccount(
34
34
  CommandContext calldata c
35
35
  ) external onlyCommand returns (bytes memory) {
36
- (Cur memory state, , ) = Cursors.init(c.state, 0, 1);
36
+ (Cur memory state, , ) = Cursors.init(c.state, 1);
37
37
 
38
38
  while (state.i < state.len) {
39
39
  (bytes32 asset, bytes32 meta, uint amount) = state.unpackBalance();
@@ -25,7 +25,7 @@ abstract contract DebitAccount is CommandBase, DebitAccountHook {
25
25
  uint internal immutable debitAccountId = commandId(this.debitAccount.selector);
26
26
 
27
27
  constructor() {
28
- emit Command(host, debitAccountId, "1:0:1", Schemas.Amount, Keys.Empty, Keys.Balance, false, false);
28
+ emit Command(host, debitAccountId, "1:0:1", Schemas.Amount, Keys.Empty, Keys.Balance, false);
29
29
  emit Labeled(debitAccountId, bytes32(0), "debitAccount");
30
30
  }
31
31
 
@@ -33,7 +33,7 @@ abstract contract DebitAccount is CommandBase, DebitAccountHook {
33
33
  /// The default implementation iterates AMOUNT blocks, calls
34
34
  /// `debitAccount`, and emits matching BALANCE blocks.
35
35
  function debitAccount(bytes32 account, bytes calldata request) internal virtual returns (bytes memory) {
36
- (Cur memory input, uint groups, ) = Cursors.init(request, 0, 1);
36
+ (Cur memory input, uint groups, ) = Cursors.init(request, 1);
37
37
  Writer memory writer = Writers.allocBalances(groups);
38
38
 
39
39
  while (input.i < input.len) {