@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.
- package/CHANGELOG.md +23 -0
- package/Endpoints.sol +10 -7
- package/Events.sol +1 -0
- package/README.md +300 -79
- package/blocks/Cursors.sol +49 -14
- package/blocks/Keys.sol +4 -0
- package/blocks/Schema.sol +10 -4
- package/blocks/Writers.sol +16 -0
- package/commands/Base.sol +10 -9
- package/commands/Burn.sol +4 -5
- package/commands/Credit.sol +4 -5
- package/commands/Debit.sol +4 -5
- package/commands/Deposit.sol +8 -10
- package/commands/Payout.sol +5 -6
- package/commands/Provision.sol +8 -10
- package/commands/Relay.sol +4 -5
- package/commands/Withdraw.sol +4 -5
- package/commands/admin/AllowAssets.sol +4 -5
- package/commands/admin/Allowance.sol +4 -4
- package/commands/admin/Appoint.sol +4 -5
- package/commands/admin/Authorize.sol +4 -5
- package/commands/admin/DenyAssets.sol +4 -5
- package/commands/admin/Destroy.sol +3 -4
- package/commands/admin/Dismiss.sol +4 -5
- package/commands/admin/Execute.sol +4 -5
- package/commands/admin/Init.sol +3 -4
- package/commands/admin/Label.sol +35 -0
- package/commands/admin/Unauthorize.sol +4 -5
- package/core/Host.sol +7 -11
- package/core/Pipeline.sol +1 -1
- package/docs/Schema.md +59 -2
- package/events/Admin.sol +3 -9
- package/events/Chain.sol +2 -3
- package/events/Command.sol +3 -9
- package/events/Guard.sol +2 -3
- package/events/Introduction.sol +2 -4
- package/events/Labeled.sol +21 -0
- package/events/Peer.sol +2 -11
- package/events/Query.sol +2 -3
- package/events/Transfer.sol +1 -1
- package/guards/Base.sol +7 -6
- package/guards/Revoke.sol +4 -5
- package/package.json +1 -1
- package/peer/AllowAssets.sol +9 -5
- package/peer/Allowance.sol +9 -5
- package/peer/BalancePull.sol +9 -5
- package/peer/Base.sol +7 -6
- package/peer/Credit.sol +39 -0
- package/peer/Debit.sol +39 -0
- package/peer/DenyAssets.sol +9 -5
- package/peer/Dispatch.sol +9 -5
- package/peer/Pipe.sol +9 -5
- package/peer/Settle.sol +9 -5
- package/queries/Assets.sol +4 -4
- package/queries/Balances.sol +4 -4
- package/queries/Base.sol +9 -8
- 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 {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
## Quick Start
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
68
|
+
## Blocks
|
|
28
69
|
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
74
|
+
```txt
|
|
75
|
+
[bytes4 key][uint32 payloadLen][payload]
|
|
76
|
+
```
|
|
34
77
|
|
|
35
|
-
|
|
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
|
-
|
|
81
|
+
```txt
|
|
82
|
+
#amount { bytes32 asset, bytes32 meta, uint amount }
|
|
83
|
+
```
|
|
38
84
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
## IDs, Accounts, and Assets
|
|
44
128
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
132
|
+
```txt
|
|
133
|
+
[uint32 type][uint32 chainid][192-bit payload]
|
|
134
|
+
```
|
|
50
135
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
152
|
+
## Hosts
|
|
59
153
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
import { Cursors, Cur, Schemas, Writer, Writers } from "@rootzero/contracts/Cursors.sol";
|
|
175
|
+
## Commands
|
|
70
176
|
|
|
71
|
-
|
|
72
|
-
using Writers for Writer;
|
|
177
|
+
Commands are the write endpoints. Every command receives the same context:
|
|
73
178
|
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
215
|
+
```solidity
|
|
216
|
+
abstract contract MyCommand is CommandBase {
|
|
217
|
+
uint internal immutable myCommandId = commandId(this.myCommand.selector);
|
|
93
218
|
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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.
|
package/blocks/Cursors.sol
CHANGED
|
@@ -70,41 +70,37 @@ library Cursors {
|
|
|
70
70
|
return open(source[i:]);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/// @notice Create a cursor over `source
|
|
74
|
-
/// Equivalent to `open(source
|
|
75
|
-
/// @param source Calldata slice that forms the
|
|
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
|
|
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
|
|
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 =
|
|
88
|
+
next = cur.len;
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
/// @notice Create a cursor over `source
|
|
94
|
-
/// @param source Calldata slice that forms the
|
|
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
|
|
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,
|
|
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"));
|