@rootzero/contracts 1.1.0 → 1.3.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 (57) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/Endpoints.sol +10 -7
  3. package/Events.sol +1 -0
  4. package/README.md +300 -79
  5. package/blocks/Cursors.sol +49 -14
  6. package/blocks/Keys.sol +4 -0
  7. package/blocks/Schema.sol +10 -4
  8. package/blocks/Writers.sol +16 -0
  9. package/commands/Base.sol +10 -9
  10. package/commands/Burn.sol +4 -5
  11. package/commands/Credit.sol +4 -5
  12. package/commands/Debit.sol +4 -5
  13. package/commands/Deposit.sol +8 -10
  14. package/commands/Payout.sol +5 -6
  15. package/commands/Provision.sol +8 -10
  16. package/commands/Relay.sol +4 -5
  17. package/commands/Withdraw.sol +4 -5
  18. package/commands/admin/AllowAssets.sol +4 -5
  19. package/commands/admin/Allowance.sol +4 -4
  20. package/commands/admin/Appoint.sol +4 -5
  21. package/commands/admin/Authorize.sol +4 -5
  22. package/commands/admin/DenyAssets.sol +4 -5
  23. package/commands/admin/Destroy.sol +3 -4
  24. package/commands/admin/Dismiss.sol +4 -5
  25. package/commands/admin/Execute.sol +4 -5
  26. package/commands/admin/Init.sol +3 -4
  27. package/commands/admin/Label.sol +35 -0
  28. package/commands/admin/Unauthorize.sol +4 -5
  29. package/core/Host.sol +7 -11
  30. package/core/Pipeline.sol +1 -1
  31. package/docs/Schema.md +59 -2
  32. package/events/Admin.sol +3 -9
  33. package/events/Chain.sol +2 -3
  34. package/events/Command.sol +3 -9
  35. package/events/Guard.sol +2 -3
  36. package/events/Introduction.sol +2 -4
  37. package/events/Labeled.sol +21 -0
  38. package/events/Peer.sol +2 -11
  39. package/events/Query.sol +2 -3
  40. package/events/Transfer.sol +1 -1
  41. package/guards/Base.sol +7 -6
  42. package/guards/Revoke.sol +4 -5
  43. package/package.json +1 -1
  44. package/peer/AllowAssets.sol +9 -5
  45. package/peer/Allowance.sol +9 -5
  46. package/peer/BalancePull.sol +9 -5
  47. package/peer/Base.sol +7 -6
  48. package/peer/Credit.sol +39 -0
  49. package/peer/Debit.sol +39 -0
  50. package/peer/DenyAssets.sol +9 -5
  51. package/peer/Dispatch.sol +9 -5
  52. package/peer/Pipe.sol +9 -5
  53. package/peer/Settle.sol +9 -5
  54. package/queries/Assets.sol +4 -4
  55. package/queries/Balances.sol +4 -4
  56. package/queries/Base.sol +9 -8
  57. package/queries/Positions.sol +4 -5
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
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.3.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - 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)`.
11
+ - Tightened command, query, and peer request parsing around the single-run convention used by current protocol endpoints.
12
+
13
+ ### Added
14
+
15
+ - Added `peerCreditTo` and `peerDebitFrom` peer endpoints for account-scoped `ACCOUNT_AMOUNT` batches.
16
+ - Added same-file `IPeer*` interfaces for every peer endpoint and exported them from `Endpoints.sol`.
17
+ - Added indexing documentation covering discovery, labels, access state, balance events, and flow-event conventions.
18
+
19
+ ## 1.2.0
20
+
21
+ ### Breaking Changes
22
+
23
+ - Removed human-readable names from Command, Admin, Peer, Query, Guard, and Chain discovery events.
24
+ - Added the Labeled event and default labels for commands, admin commands, peers, queries, guards, and examples.
25
+ - Added LABEL and STRING block schemas, plus cursor helpers for decoding labels supplied by callers.
26
+ - Added the admin label command for publishing mutable namespaced labels.
27
+ - Removed version and namespace fields from host Introduction events and host constructor introductions.
28
+
6
29
  ## 1.1.0
7
30
 
8
31
  ### Breaking Changes
package/Endpoints.sol CHANGED
@@ -29,17 +29,20 @@ import { DenyAssets, DenyAssetsHook } from "./commands/admin/DenyAssets.sol";
29
29
  import { Dismiss } from "./commands/admin/Dismiss.sol";
30
30
  import { ExecutePayable } from "./commands/admin/Execute.sol";
31
31
  import { Init, InitHook } from "./commands/admin/Init.sol";
32
+ import { Label } from "./commands/admin/Label.sol";
32
33
  import { Unauthorize } from "./commands/admin/Unauthorize.sol";
33
34
 
34
35
  // Peer endpoints
35
36
  import { PeerBase, encodePeerCall } from "./peer/Base.sol";
36
- import { PeerAllowAssets } from "./peer/AllowAssets.sol";
37
- import { PeerAllowance } from "./peer/Allowance.sol";
38
- import { PeerBalancePull, BalancePullHook } from "./peer/BalancePull.sol";
39
- import { PeerDenyAssets } from "./peer/DenyAssets.sol";
40
- import { PeerPipePayable } from "./peer/Pipe.sol";
41
- import { PeerDispatchPayable } from "./peer/Dispatch.sol";
42
- 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";
43
46
 
44
47
  // Guard endpoints
45
48
  import { GuardBase, encodeGuardCall } from "./guards/Base.sol";
package/Events.sol CHANGED
@@ -16,6 +16,7 @@ import { EventEmitter } from "./events/Emitter.sol";
16
16
  import { GuardEvent } from "./events/Guard.sol";
17
17
  import { GuardianEvent } from "./events/Guardian.sol";
18
18
  import { IntroductionEvent } from "./events/Introduction.sol";
19
+ import { LabeledEvent } from "./events/Labeled.sol";
19
20
  import { LockedEvent } from "./events/Locked.sol";
20
21
  import { NodeEvent } from "./events/Node.sol";
21
22
  import { PeerEvent } from "./events/Peer.sol";
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
 
@@ -489,6 +485,13 @@ library Cursors {
489
485
  return createBlock(Keys.Bytes, data);
490
486
  }
491
487
 
488
+ /// @notice Encode a STRING block with a UTF-8 payload.
489
+ /// @param data String payload.
490
+ /// @return Encoded STRING block bytes.
491
+ function toStringBlock(string memory data) internal pure returns (bytes memory) {
492
+ return createBlock(Keys.String, bytes(data));
493
+ }
494
+
492
495
  /// @notice Encode a BOUNTY block.
493
496
  /// @param bounty Relayer reward amount.
494
497
  /// @param relayer Relayer account identifier.
@@ -610,6 +613,18 @@ library Cursors {
610
613
  cur.i += 4;
611
614
  }
612
615
 
616
+ /// @notice Read the next 8 bytes from the cursor and advance by 8 bytes.
617
+ /// @dev Performs no bounds, key, length, or cursor checks.
618
+ /// @param cur Cursor whose current position is advanced by 8 bytes.
619
+ /// @return value Loaded bytes8 value.
620
+ function read8(Cur memory cur) internal pure returns (bytes8 value) {
621
+ uint abs = cur.offset + cur.i;
622
+ assembly ("memory-safe") {
623
+ value := calldataload(abs)
624
+ }
625
+ cur.i += 8;
626
+ }
627
+
613
628
  /// @notice Read the next 16 bytes from the cursor and advance by 16 bytes.
614
629
  /// @dev Performs no bounds, key, length, or cursor checks.
615
630
  /// @param cur Cursor whose current position is advanced by 16 bytes.
@@ -708,6 +723,26 @@ library Cursors {
708
723
  return unpackRaw(cur, Keys.Bytes);
709
724
  }
710
725
 
726
+ /// @notice Consume a reserved STRING block and return its UTF-8 payload.
727
+ /// @param cur Cursor; advanced past the STRING block.
728
+ /// @return data Decoded STRING payload.
729
+ function unpackString(Cur memory cur) internal pure returns (string memory data) {
730
+ return string(unpackRaw(cur, Keys.String));
731
+ }
732
+
733
+ /// @notice Consume a LABEL block and return its fields.
734
+ /// @param cur Cursor; advanced past the LABEL block.
735
+ /// @return id Node ID being labelled.
736
+ /// @return namespace Label namespace.
737
+ /// @return name Label value.
738
+ function unpackLabel(Cur memory cur) internal pure returns (uint id, bytes32 namespace, string memory name) {
739
+ uint end = cur.enter(Keys.Label, 64 + Sizes.Header, 0);
740
+ id = cur.readUint();
741
+ namespace = cur.read32();
742
+ name = cur.unpackString();
743
+ cur.exit(end);
744
+ }
745
+
711
746
  /// @notice Consume a dynamic block with a single bytes32 payload.
712
747
  /// @param cur Cursor; advanced past the block.
713
748
  /// @param key Expected dynamic block key.
package/blocks/Keys.sol CHANGED
@@ -33,6 +33,8 @@ library Keys {
33
33
  bytes4 constant Evm = bytes4(keccak256("#evm"));
34
34
  /// @dev Reserved raw bytes child block.
35
35
  bytes4 constant Bytes = bytes4(keccak256("#bytes"));
36
+ /// @dev Reserved UTF-8 string child block.
37
+ bytes4 constant String = bytes4(keccak256("#string"));
36
38
  /// @dev Account identifier - (bytes32 account)
37
39
  bytes4 constant Account = bytes4(keccak256("#account"));
38
40
  /// @dev Transfer record passed through the pipeline - (bytes32 from, bytes32 to, bytes32 asset, bytes32 meta, uint amount)
@@ -57,6 +59,8 @@ library Keys {
57
59
  bytes4 constant Node = bytes4(keccak256("#node"));
58
60
  /// @dev Relayer bounty - (uint amount, bytes32 relayer)
59
61
  bytes4 constant Bounty = bytes4(keccak256("#bounty"));
62
+ /// @dev Mutable node label - (uint id, bytes32 namespace, #string as name)
63
+ bytes4 constant Label = bytes4(keccak256("#label"));
60
64
 
61
65
  /// @dev Structural status form - (uint code)
62
66
  bytes4 constant Status = bytes4(keccak256("#status"));