@rootzero/contracts 1.15.0 → 1.16.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 (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/Codec.sol +1 -1
  3. package/Commands.sol +1 -1
  4. package/Core.sol +2 -2
  5. package/Endpoints.sol +5 -4
  6. package/Events.sol +0 -1
  7. package/README.md +82 -28
  8. package/codec/Blocks.sol +79 -21
  9. package/codec/Buffers.sol +3 -3
  10. package/codec/Decoders.sol +31 -10
  11. package/codec/Descriptors.sol +0 -1
  12. package/codec/Keys.sol +2 -2
  13. package/codec/Readers.sol +13 -0
  14. package/codec/Schema.sol +5 -1
  15. package/codec/Specs.sol +4 -2
  16. package/codec/Writers.sol +19 -2
  17. package/commands/Base.sol +6 -15
  18. package/commands/Burn.sol +2 -2
  19. package/commands/Credit.sol +35 -3
  20. package/commands/Debit.sol +46 -13
  21. package/commands/Deposit.sol +4 -4
  22. package/commands/Provision.sol +4 -4
  23. package/commands/Recover.sol +15 -8
  24. package/commands/Relay.sol +37 -12
  25. package/commands/Settle.sol +77 -0
  26. package/commands/Withdraw.sol +2 -2
  27. package/commands/admin/AllowAssets.sol +2 -2
  28. package/commands/admin/Allowance.sol +2 -2
  29. package/commands/admin/Annotate.sol +2 -2
  30. package/commands/admin/Appoint.sol +2 -2
  31. package/commands/admin/Authorize.sol +2 -2
  32. package/commands/admin/DenyAssets.sol +2 -2
  33. package/commands/admin/Dismiss.sol +2 -2
  34. package/commands/admin/Execute.sol +3 -3
  35. package/commands/admin/Unauthorize.sol +2 -2
  36. package/core/Endpoint.sol +15 -13
  37. package/core/Pipeline.sol +6 -6
  38. package/core/Settlement.sol +37 -8
  39. package/core/Types.sol +13 -1
  40. package/docs/Schema.md +52 -2
  41. package/execution/Budget.sol +12 -4
  42. package/execution/Execution.sol +85 -11
  43. package/guards/Base.sol +2 -2
  44. package/package.json +1 -1
  45. package/ports/Base.sol +2 -2
  46. package/ports/Dispatch.sol +6 -6
  47. package/ports/{Settle.sol → Post.sol} +10 -10
  48. package/queries/Base.sol +2 -2
  49. package/utils/Actions.sol +1 -0
  50. package/utils/Cursors.sol +16 -30
  51. package/events/Position.sol +0 -22
package/CHANGELOG.md CHANGED
@@ -3,6 +3,64 @@
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.16.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - Removed the unused generic `Position` event and `PositionEvent` base contract.
11
+ - Renamed transaction handling from settlement to posting: the transaction
12
+ helper is now `post(...)`, and `portSettle(bytes)` is now `portPost(bytes)`.
13
+ The port selector and node ID change, and its action is now `Actions.Post`
14
+ (`14`) instead of `Actions.Settle` (`3`). The `Settlement` convenience base
15
+ implements both the transaction `PostHook` and position `SettleHook`.
16
+ - Removed `openInput` from `EndpointBase`. Port, query, and guard bases expose
17
+ `openInput` through the input-only `InputEndpointBase`, while custom commands
18
+ must pass both state and input through `openCommand`.
19
+ - Removed the wildcard `Specs.Any` state type. The stateful relay is now
20
+ `relayBalancePayable` and accepts `BALANCE` state blocks; `relayPayable`
21
+ explicitly accepts empty state.
22
+ - Renamed the numeric `Position` fields from `assets` and `liabilities` to
23
+ `amount` and `debt`.
24
+ - Renamed `Budget.use` to `useResourceValue` and split execution spending into
25
+ exact `useValue` and packed-resource `useResourceValue` helpers.
26
+ - Renamed `RoutePayableHook.route` to `RelayPayableHook.relayTo`. Recovery hooks
27
+ are now `RecoverPayableHook` implementations that receive the complete
28
+ resource word and mutable execution budget.
29
+
30
+ ### Added
31
+
32
+ - Added the hostless `#position` state block for threading asset-liability pairs
33
+ between pipeline commands.
34
+ - Added the `settle` command, the position `SettleHook`, and the transaction
35
+ `PostHook`. The `Settlement` convenience base provides default implementations
36
+ of both through the debit and credit account hooks.
37
+ - Added `relayBalancePayable` for relaying required `BALANCE` state while
38
+ `relayPayable` now explicitly relays with empty state.
39
+ - Added memory-backed `InternalDebitAccount`, `InternalCreditAccount`, and
40
+ `InternalSettle` adapters for hosts that execute canonical commands directly
41
+ from their pipeline dispatcher.
42
+
43
+ ### Changed
44
+
45
+ - Documented the command state-safety invariant: every command must handle the
46
+ complete supplied state stream or revert, and commands with empty state lanes
47
+ must reject non-empty state.
48
+ - Endpoint decoder opening now requires the complete supplied lane to be one
49
+ homogeneous run of the descriptor's declared key; trailing block types are
50
+ rejected instead of silently left outside the decoder cursor.
51
+ - Block runs and cursors now retain raw block counts only. Descriptor stride
52
+ conversion and state/input group reconciliation happen once in execution
53
+ opening rather than in `Blocks` or `Cursors`.
54
+ - Pipeline transaction streams are posted before the next step, and position
55
+ state is settled separately through the scalar asset, amount, liability, and
56
+ debt hook.
57
+
58
+ ### Upgrade Compatibility
59
+
60
+ - Command and port selectors, block schemas, and hook signatures changed in
61
+ this release. Deploy fresh hosts and update pipeline builders and peer
62
+ integrations together.
63
+
6
64
  ## 1.15.0
7
65
 
8
66
  ### Breaking Changes
package/Codec.sol CHANGED
@@ -4,7 +4,7 @@ pragma solidity ^0.8.33;
4
4
  // Aggregator: re-exports the complete block encoding and decoding surface.
5
5
  // Import this file for low-level codec extensions and direct stream processing.
6
6
 
7
- import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx } from "./core/Types.sol";
7
+ import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Position, Tx } from "./core/Types.sol";
8
8
  import { Keys } from "./codec/Keys.sol";
9
9
  import { Sizes, Specs } from "./codec/Specs.sol";
10
10
  import { Descriptors } from "./codec/Descriptors.sol";
package/Commands.sol CHANGED
@@ -11,4 +11,4 @@ import {Blocks} from "./codec/Blocks.sol";
11
11
  import {Sizes, Specs} from "./codec/Specs.sol";
12
12
  import {Decoders} from "./codec/Decoders.sol";
13
13
  import {Cursors, Cur} from "./utils/Cursors.sol";
14
- import {AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx} from "./core/Types.sol";
14
+ import {AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Position, Tx} from "./core/Types.sol";
package/Core.sol CHANGED
@@ -16,9 +16,9 @@ import { CommandCalls, FailedCall, NodeCalls, PortCalls, RawNodeCalls } from "./
16
16
  import { EndpointBase } from "./core/Endpoint.sol";
17
17
  import { Pipeline } from "./core/Pipeline.sol";
18
18
  import { Budget, Budgets } from "./execution/Budget.sol";
19
- import { CreditAccountHook, DebitAccountHook, Settlement } from "./core/Settlement.sol";
19
+ import { CreditAccountHook, DebitAccountHook, PostHook, SettleHook, Settlement } from "./core/Settlement.sol";
20
20
  import { Portal } from "./core/Portal.sol";
21
- import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx } from "./core/Types.sol";
21
+ import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Position, Tx } from "./core/Types.sol";
22
22
  import { Validator } from "./core/Validator.sol";
23
23
 
24
24
 
package/Endpoints.sol CHANGED
@@ -5,7 +5,7 @@ pragma solidity ^0.8.33;
5
5
  // Import this file to inherit from the full rootzero callable host surface without managing individual paths.
6
6
 
7
7
  // Shared endpoint hooks
8
- import { CreditAccountHook, DebitAccountHook } from "./core/Settlement.sol";
8
+ import { CreditAccountHook, DebitAccountHook, PostHook, SettleHook } from "./core/Settlement.sol";
9
9
 
10
10
  // Commands
11
11
  import { Allocate, AllocateHook } from "./commands/Allocate.sol";
@@ -15,8 +15,9 @@ import { DebitAccount } from "./commands/Debit.sol";
15
15
  import { Deposit, DepositHook, DepositPayable, DepositPayableHook } from "./commands/Deposit.sol";
16
16
  import { Payout, PayoutHook } from "./commands/Payout.sol";
17
17
  import { Provision, ProvisionHook, ProvisionPayable, ProvisionPayableHook } from "./commands/Provision.sol";
18
- import { RecoverHook, RecoverPayable } from "./commands/Recover.sol";
19
- import { RelayPayable, RoutePayableHook } from "./commands/Relay.sol";
18
+ import { RecoverPayable, RecoverPayableHook } from "./commands/Recover.sol";
19
+ import { RelayPayable, RelayBalancePayable, RelayPayableHook } from "./commands/Relay.sol";
20
+ import { Settle } from "./commands/Settle.sol";
20
21
  import { Withdraw, WithdrawHook } from "./commands/Withdraw.sol";
21
22
 
22
23
  // Admin commands
@@ -41,7 +42,7 @@ import { PortDebitAccount } from "./ports/Debit.sol";
41
42
  import { PortDenyAssets } from "./ports/DenyAssets.sol";
42
43
  import { PortPipePayable } from "./ports/Pipe.sol";
43
44
  import { PortDispatchPayable } from "./ports/Dispatch.sol";
44
- import { PortSettle } from "./ports/Settle.sol";
45
+ import { PortPost } from "./ports/Post.sol";
45
46
 
46
47
  // Guard endpoints
47
48
  import { GuardBase } from "./guards/Base.sol";
package/Events.sol CHANGED
@@ -11,7 +11,6 @@ import { BalanceEvent } from "./events/Balance.sol";
11
11
  import { CommanderEvent } from "./events/Commander.sol";
12
12
  import { DispatchEvent } from "./events/Dispatch.sol";
13
13
  import { EndpointEvent } from "./events/Endpoint.sol";
14
- import { PositionEvent } from "./events/Position.sol";
15
14
  import { ReceivedEvent } from "./events/Received.sol";
16
15
  import { RecoveredEvent } from "./events/Recovered.sol";
17
16
  import { EventEmitter } from "./events/Emitter.sol";
package/README.md CHANGED
@@ -149,7 +149,7 @@ const input = concat([
149
149
  ```
150
150
 
151
151
  Everything downstream keeps this shape: commands loop over input blocks,
152
- settlement loops over transactions, pipelines loop over steps. Batching is
152
+ posting loops over transactions, pipelines loop over steps. Batching is
153
153
  never a special case.
154
154
 
155
155
  ## IDs, Accounts, Assets, and Nodes
@@ -248,30 +248,51 @@ struct CommandContext {
248
248
 
249
249
  Every command returns two block streams: `state`, which is threaded into the
250
250
  next pipeline step, and `transactions`, which contains `#transaction` blocks
251
- for the pipeline host to settle outside the state lane. Either stream may be
251
+ for the pipeline host to post outside the state lane. Either stream may be
252
252
  empty.
253
253
 
254
- The input carries instructions; the state carries live value. While a
255
- sequence of commands executes, `#balance` and `#custody` blocks in the state
256
- are the funds being moved produced by one command, consumed by the next.
254
+ State is linear, not optional ambient context. A command is responsible for
255
+ the entire state stream it receives: it must validate and consume it, transform
256
+ and return it, forward it intact, or revert. A command must never succeed while
257
+ silently ignoring or dropping supplied state. Commands that declare
258
+ `Specs.Empty` state therefore reject any non-empty state, while commands that
259
+ accept state validate the complete stream against their declared state schema.
260
+ This is especially important for `#position`, because dropping a position could
261
+ silently discard both live value and an outstanding debt requirement.
257
262
 
258
- The standard `Deposit` mixin shows the canonical shape open the input,
259
- loop the batch, call the hook, write the output run:
263
+ The input carries instructions; the state carries live value. While a
264
+ sequence of commands executes, `#balance`, `#custody`, and `#position` blocks
265
+ in the state are the value being moved — produced by one command, consumed by
266
+ the next. A position carries an asset-liability pair as
267
+ `{ asset, amount, liability, debt }`.
268
+
269
+ `#position` is general live state rather than a lending-specific debt record.
270
+ It pairs value acquired or controlled with value owed or required. A command
271
+ may preserve or replace either side and return the resulting position for the
272
+ next step; `settle` terminally consumes the pair. This supports swaps,
273
+ borrowing, refinancing, collateral changes, callback obligations, cross-host
274
+ claims, fees, netting, and other multi-step operations. A position is a
275
+ transient representation and does not itself create or erase an obligation
276
+ recorded by an external system.
277
+
278
+ The standard `Deposit` mixin shows the canonical shape: open and validate both
279
+ command lanes, loop the batch, call the hook, and write the output run:
260
280
 
261
281
  ```solidity
262
282
  function deposit(
263
- CommandContext calldata c
283
+ bytes32 account,
284
+ bytes calldata state,
285
+ bytes calldata input
264
286
  ) external onlyCommand returns (bytes memory, bytes memory) {
265
- (Cur memory input, uint outputs) = openInput(c.input, descriptor);
266
- Writer memory output = Writers.allocBalances(outputs);
287
+ Execution memory exec = openCommand(state, input, descriptor, 0);
267
288
 
268
- while (input.i < input.len) {
269
- (bytes32 asset, uint amount) = input.unpackAmount();
270
- deposit(c.account, asset, amount); // host policy hook
271
- output.appendBalance(asset, amount);
289
+ while (exec.more()) {
290
+ (bytes32 asset, uint amount) = exec.unpackAmount(Lanes.Input);
291
+ deposit(account, asset, amount); // host policy hook
292
+ exec.outputBalance(asset, amount);
272
293
  }
273
294
 
274
- return (end(output), "");
295
+ return close(exec, account);
275
296
  }
276
297
  ```
277
298
 
@@ -292,7 +313,13 @@ abstract contract MyCommand is CommandBase {
292
313
  bytes calldata state,
293
314
  bytes calldata input
294
315
  ) external onlyCommand returns (bytes memory, bytes memory) {
295
- // parse input, loop, return the output state run and any transactions
316
+ Execution memory exec = openCommand(state, input, descriptor, 0);
317
+ while (exec.more()) {
318
+ (bytes32 asset, uint amount) = exec.unpackAmount(Lanes.Input);
319
+ // Apply command-specific behavior for this group.
320
+ exec.outputBalance(asset, amount);
321
+ }
322
+ return close(exec, account);
296
323
  }
297
324
  }
298
325
  ```
@@ -301,8 +328,10 @@ The standard commands cover the common ledger movements: `deposit` and
301
328
  `depositPayable` (external funds in), `withdraw` and `burn` (funds out),
302
329
  `debitAccount` and `creditAccount` (internal movements), `payout` (deliver
303
330
  state to other accounts), `allocate` (turn balance state into custody),
304
- `provision` (provision custody from an external allocation), and `relayPayable`
305
- (hand a pipeline to another portal).
331
+ `provision` (provision custody from an external allocation), `settle` (consume
332
+ asset-liability position state), `relayPayable` (relay a pipeline without
333
+ state), and `relayBalancePayable` (relay balance state and a pipeline to another
334
+ portal).
306
335
 
307
336
  ## Pipelines
308
337
 
@@ -316,7 +345,7 @@ step { uint cmd, uint resources, #bytes as input }
316
345
  Each step names a command, the resources it may spend, and its input.
317
346
  The returned state threads into the next command and the final state must be
318
347
  empty. Returned transactions do not enter the state lane; the pipeline passes
319
- each decoded transaction to the shared settlement implementation before
348
+ each decoded transaction to the shared posting implementation before
320
349
  running the next step. This is the core of `Pipeline.pipe`:
321
350
 
322
351
  ```solidity
@@ -328,11 +357,11 @@ while (cur.more()) {
328
357
  account,
329
358
  state,
330
359
  input,
331
- budget.use(resources)
360
+ budget.useResourceValue(resources)
332
361
  );
333
362
  while (transactions.more()) {
334
363
  (bytes32 from, bytes32 to, bytes32 asset, uint amount) = transactions.unpackTransaction();
335
- settle(from, to, asset, amount);
364
+ post(from, to, asset, amount);
336
365
  }
337
366
  }
338
367
  if (state.length != 0) revert UnexpectedState();
@@ -345,6 +374,31 @@ command batching — and `resources` is a chain-specific word interpreted by the
345
374
  portal adapter (on EVM, the low 128 bits are native value in wei, drawn from a
346
375
  shared budget), so the same pipeline bytes are meaningful to every port.
347
376
 
377
+ Hosts that implement a pipeline locally can inherit `InternalDebitAccount`,
378
+ `InternalCreditAccount`, and `InternalSettle` to advertise the canonical command
379
+ endpoints while routing their local command IDs through `executeDebitAccount`,
380
+ `executeCreditAccount`, and `executeSettle`. These adapters consume the
381
+ memory-backed pipeline state directly and avoid an external self-call. The host
382
+ dispatcher must reject nonzero step value before invoking them because all three
383
+ commands are non-funded.
384
+
385
+ Positions also support backward-composed pipelines. In an exact-output route,
386
+ the asset side can represent the desired result while the liability side
387
+ represents the value currently required upstream. Each hop consumes one
388
+ position, fulfills or transforms its current requirement, and returns the next
389
+ position:
390
+
391
+ ```txt
392
+ position(C, 100, C, 100)
393
+ → position(C, 100, B, 50)
394
+ → position(C, 100, A, 25)
395
+ → settle
396
+ ```
397
+
398
+ This is backward composition, not backward execution: `#step` blocks still
399
+ execute forward in their encoded order. Exact-output routing is only one use;
400
+ other commands may transform the asset side, the liability side, or both.
401
+
348
402
  ## Queries
349
403
 
350
404
  Queries are the read endpoints: view functions that take a block-stream input
@@ -365,15 +419,15 @@ the descriptor's lanes through the published block schemas.
365
419
  Ports are the host-to-host surfaces, callable only by trusted peer hosts. The two
366
420
  central ones are batches all the way down:
367
421
 
368
- - `portSettle` consumes `transaction { bytes32 from, bytes32 to, bytes32 asset,
422
+ - `portPost` consumes `transaction { bytes32 from, bytes32 to, bytes32 asset,
369
423
  uint amount }` blocks, debiting `from` and crediting `to` per
370
- block — how two hosts record settlement between their ledgers.
424
+ block — how two hosts post transactions between their ledgers.
371
425
  - `portPipePayable` consumes `context` blocks, each carrying an account, an
372
426
  initial state, and a run of steps — a complete pipeline delivered by another
373
427
  host, executed locally against the port call's shared value budget.
374
428
 
375
- This is also the cross-portal mechanism. `relayPayable` (or `portDispatchPayable`)
376
- wraps a pipe and addresses it to a portal, commonly the destination host ID;
429
+ This is also the cross-portal mechanism. `relayPayable`, `relayBalancePayable`,
430
+ or `portDispatchPayable` wraps a pipe and addresses it to a portal, commonly the destination host ID;
377
431
  a bridge adapter moves the **raw
378
432
  bytes**; the destination host parses them with the same cursor rules and runs
379
433
  the same pipeline loop. Nothing in the payload is EVM-specific — step commands
@@ -386,9 +440,9 @@ bytes and produce the same output bytes for every endpoint.
386
440
 
387
441
  Admin commands use the regular command shape but are gated to the host's admin
388
442
  account: trust management (`authorize`, `unauthorize`), guardian management
389
- (`appoint`, `dismiss`), metadata (`label`, `publishSchema`), asset gating (`allowAssets`,
390
- `denyAssets`, `allowance`), lifecycle (`init`, `destroy`), and raw calls
391
- (`executePayable`). Guards go the other way: direct actions guardians can take
443
+ (`appoint`, `dismiss`), metadata (`annotate`), optional asset gating
444
+ (`allowAssets`, `denyAssets`, `allowance`), and raw calls (`executePayable`).
445
+ Guards go the other way: direct actions guardians can take
392
446
  without any command context — the default is `revoke`, which lets a guardian
393
447
  drop a trusted node immediately.
394
448
 
package/codec/Blocks.sol CHANGED
@@ -35,10 +35,6 @@ library Blocks {
35
35
  error UnexpectedValue();
36
36
  /// @dev A scoped block run contained no blocks.
37
37
  error EmptyRun();
38
- /// @dev A block run was scoped with a zero stride.
39
- error ZeroStride();
40
- /// @dev A block count is not divisible by its declared stride.
41
- error BadRatio();
42
38
 
43
39
  // -------------------------------------------------------------------------
44
40
  // Calldata inspection and navigation
@@ -147,25 +143,16 @@ library Blocks {
147
143
  }
148
144
  }
149
145
 
150
- /// @notice Scope a consecutive block run into equal-sized groups.
146
+ /// @notice Count a run that must consume the complete region.
147
+ /// @dev Reverts when any well-formed trailing block has a different key.
151
148
  /// @param abs Absolute start position.
152
149
  /// @param limit Absolute region boundary.
153
- /// @param key Block key forming the run.
154
- /// @param stride Number of blocks per group.
155
- /// @return groups Number of complete groups in the run.
156
- /// @return end Absolute position immediately after the run.
157
- function scope(
158
- uint abs,
159
- uint limit,
160
- bytes4 key,
161
- uint stride
162
- ) internal pure returns (uint groups, uint end) {
163
- if (stride == 0) revert ZeroStride();
164
- uint count;
165
- (count, end) = run(abs, limit, key);
166
- if (count == 0) revert EmptyRun();
167
- if (count % stride != 0) revert BadRatio();
168
- groups = count / stride;
150
+ /// @param key Required block key for the complete region.
151
+ /// @return total Number of matching blocks.
152
+ /// @return end Absolute position equal to `limit`.
153
+ function runExact(uint abs, uint limit, bytes4 key) internal pure returns (uint total, uint end) {
154
+ (total, end) = run(abs, limit, key);
155
+ if (end != limit) revert InvalidBlock();
169
156
  }
170
157
 
171
158
  // Generic block writes
@@ -460,6 +447,33 @@ library Blocks {
460
447
 
461
448
  // Four-word payloads
462
449
 
450
+ /// @notice Write a POSITION block at `i`.
451
+ /// @dev DANGER: Unchecked memory write. Reserve `Sizes.B128` bytes first.
452
+ /// @param dst Destination buffer.
453
+ /// @param i Relative write position.
454
+ /// @param asset Identifier for the asset side.
455
+ /// @param amount Quantity on the asset side.
456
+ /// @param liability Identifier for the liability side.
457
+ /// @param debt Quantity owed on the liability side.
458
+ function writePosition(
459
+ bytes memory dst,
460
+ uint i,
461
+ bytes32 asset,
462
+ uint amount,
463
+ bytes32 liability,
464
+ uint debt
465
+ ) internal pure {
466
+ uint spec = Specs.Position;
467
+ assembly ("memory-safe") {
468
+ let p := add(add(dst, 0x20), i)
469
+ mstore(p, spec)
470
+ mstore(add(p, 0x08), asset)
471
+ mstore(add(p, 0x28), amount)
472
+ mstore(add(p, 0x48), liability)
473
+ mstore(add(p, 0x68), debt)
474
+ }
475
+ }
476
+
463
477
  /// @notice Write a TRANSACTION block at `i`.
464
478
  /// @dev DANGER: Unchecked memory write. Reserve `Sizes.B128` bytes first.
465
479
  /// @param dst Destination buffer.
@@ -1249,6 +1263,28 @@ library Blocks {
1249
1263
 
1250
1264
  // Four-word payloads
1251
1265
 
1266
+ /// @notice Decode a low-level fixed-width POSITION block at `abs`.
1267
+ /// @param abs Absolute block position.
1268
+ /// @return asset Decoded asset-side identifier.
1269
+ /// @return amount Decoded asset-side quantity.
1270
+ /// @return liability Decoded liability-side identifier.
1271
+ /// @return debt Decoded liability-side debt.
1272
+ function unpackPosition(
1273
+ uint abs
1274
+ ) internal pure returns (bytes32 asset, uint amount, bytes32 liability, uint debt) {
1275
+ uint head;
1276
+ assembly ("memory-safe") {
1277
+ head := calldataload(abs)
1278
+ }
1279
+ if (head >> 192 != Specs.Position >> 192) revert InvalidBlock();
1280
+ assembly ("memory-safe") {
1281
+ asset := calldataload(add(abs, 0x08))
1282
+ amount := calldataload(add(abs, 0x28))
1283
+ liability := calldataload(add(abs, 0x48))
1284
+ debt := calldataload(add(abs, 0x68))
1285
+ }
1286
+ }
1287
+
1252
1288
  /// @notice Decode a low-level fixed-width TRANSACTION block at `abs`.
1253
1289
  /// @param abs Absolute block position.
1254
1290
  /// @return from Decoded debit account.
@@ -1577,6 +1613,28 @@ library Blocks {
1577
1613
  return bytes.concat(Keys.Balance, bytes4(uint32(64)), asset, bytes32(amount));
1578
1614
  }
1579
1615
 
1616
+ /// @notice Encode a POSITION block.
1617
+ /// @param asset Identifier for the asset side.
1618
+ /// @param amount Quantity on the asset side.
1619
+ /// @param liability Identifier for the liability side.
1620
+ /// @param debt Quantity owed on the liability side.
1621
+ /// @return Encoded POSITION block bytes.
1622
+ function position(
1623
+ bytes32 asset,
1624
+ uint amount,
1625
+ bytes32 liability,
1626
+ uint debt
1627
+ ) internal pure returns (bytes memory) {
1628
+ return bytes.concat(
1629
+ Keys.Position,
1630
+ bytes4(uint32(128)),
1631
+ asset,
1632
+ bytes32(amount),
1633
+ liability,
1634
+ bytes32(debt)
1635
+ );
1636
+ }
1637
+
1580
1638
  /// @notice Encode a CUSTODY block.
1581
1639
  /// @param host Host node ID holding the custody.
1582
1640
  /// @param asset Asset identifier.
package/codec/Buffers.sol CHANGED
@@ -18,12 +18,12 @@ library Buffers {
18
18
 
19
19
  /// @notice Create a packed buffer cursor at write position zero.
20
20
  /// @param len Initial logical byte capacity.
21
- /// @param groups Number of logical groups represented by the buffer.
21
+ /// @param count Number of logical items represented by the buffer.
22
22
  /// @param growable Whether writes may expand the logical capacity.
23
23
  /// @param tag Cursor identity tag.
24
24
  /// @return cur Packed buffer cursor.
25
- function cursor(uint len, uint groups, bool growable, uint8 tag) internal pure returns (uint cur) {
26
- cur = Cursors.create(0, len, groups, growable ? Growable : 0, tag);
25
+ function cursor(uint len, uint count, bool growable, uint8 tag) internal pure returns (uint cur) {
26
+ cur = Cursors.create(0, len, count, growable ? Growable : 0, tag);
27
27
  }
28
28
 
29
29
  /// @notice Reserve relative write space and return the updated packed buffer cursor.
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  pragma solidity ^0.8.33;
3
3
 
4
- import {AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, Tx} from "../core/Types.sol";
4
+ import {AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, Position, Tx} from "../core/Types.sol";
5
5
  import {Blocks} from "./Blocks.sol";
6
6
  import {Sizes, Specs} from "./Specs.sol";
7
7
  import {Cursors, Cur} from "../utils/Cursors.sol";
@@ -32,20 +32,17 @@ library Decoders {
32
32
  cur.state = Cursors.wrap(source[i:], 0, 0);
33
33
  }
34
34
 
35
- /// @notice Open the first homogeneous run in `source` using `stride`.
35
+ /// @notice Open the first homogeneous run in `source`.
36
36
  /// @param source Calldata block stream to open.
37
- /// @param stride Number of blocks per group, or zero for an ungrouped stream.
38
37
  /// @return cur Cursor spanning the first homogeneous run.
39
- function open(
40
- bytes calldata source,
41
- uint stride
42
- ) internal pure returns (Cur memory cur) {
38
+ function open(bytes calldata source) internal pure returns (Cur memory cur) {
43
39
  (uint abs, uint limit) = Cursors.bounds(source);
44
- if (stride == 0 && abs == limit) return cur;
40
+ if (abs == limit) revert Blocks.EmptyRun();
45
41
 
46
42
  bytes4 key = bytes4(source);
47
- (uint groups, uint end) = Blocks.scope(abs, limit, key, stride);
48
- cur.state = Cursors.create(abs, end - abs, groups, 0, 0);
43
+ (uint count, uint end) = Blocks.run(abs, limit, key);
44
+ if (count == 0) revert Blocks.EmptyRun();
45
+ cur.state = Cursors.create(abs, end - abs, count, 0, 0);
49
46
  }
50
47
 
51
48
  /// @notice Return whether `cur` has unread bytes.
@@ -163,6 +160,16 @@ library Decoders {
163
160
  return Blocks.find(offset + i, offset + end, key) - offset;
164
161
  }
165
162
 
163
+ /// @notice Count consecutive blocks with `key` from the current cursor position.
164
+ /// @dev Does not advance the cursor.
165
+ /// @param cur Cursor to inspect.
166
+ /// @param key Block key forming the run.
167
+ /// @return count Number of consecutive matching blocks.
168
+ function run(Cur memory cur, bytes4 key) internal pure returns (uint count) {
169
+ (uint i, uint offset, uint len) = cur.state.decode();
170
+ (count, ) = Blocks.run(offset + i, offset + len, key);
171
+ }
172
+
166
173
  /// @notice Consume one LIST block and return a cursor over its items.
167
174
  /// @param cur Cursor advanced past the list.
168
175
  /// @return items Cursor spanning the list payload.
@@ -532,6 +539,20 @@ library Decoders {
532
539
  (value.host, value.asset, value.amount) = unpackCustody(cur);
533
540
  }
534
541
 
542
+ /// @notice Decode and consume one POSITION block.
543
+ function unpackPosition(
544
+ Cur memory cur
545
+ ) internal pure returns (bytes32 asset, uint amount, bytes32 liability, uint debt) {
546
+ uint abs;
547
+ (cur.state, abs) = cur.state.consume(Sizes.Position);
548
+ (asset, amount, liability, debt) = Blocks.unpackPosition(abs);
549
+ }
550
+
551
+ /// @notice Decode one POSITION block into its structured value.
552
+ function unpackPositionValue(Cur memory cur) internal pure returns (Position memory value) {
553
+ (value.asset, value.amount, value.liability, value.debt) = unpackPosition(cur);
554
+ }
555
+
535
556
  /// @notice Decode one TRANSACTION block into its structured value.
536
557
  /// @param cur Cursor advanced past the block.
537
558
  /// @return value Structured transaction.
@@ -37,7 +37,6 @@ library Descriptors {
37
37
  state = Specs.normalize(state, true);
38
38
  input = Specs.normalize(input, false);
39
39
  output = Specs.normalize(output, true);
40
-
41
40
  descriptor = pack(state, input, output, transactions, flags);
42
41
  }
43
42
 
package/codec/Keys.sol CHANGED
@@ -9,8 +9,6 @@ pragma solidity ^0.8.33;
9
9
  library Keys {
10
10
  /// @dev Empty / unset key.
11
11
  bytes4 constant Empty = bytes4(0);
12
- /// @dev Wildcard key used in discovery when any block stream is accepted.
13
- bytes4 constant Any = 0xffffffff;
14
12
  /// @dev Input amount - (bytes32 asset, uint amount)
15
13
  bytes4 constant Amount = bytes4(keccak256("#amount"));
16
14
  /// @dev Ledger balance - (bytes32 asset, uint amount)
@@ -21,6 +19,8 @@ library Keys {
21
19
  bytes4 constant Allowance = bytes4(keccak256("#allowance"));
22
20
  /// @dev Cross-host custody state - (uint host, bytes32 asset, uint amount)
23
21
  bytes4 constant Custody = bytes4(keccak256("#custody"));
22
+ /// @dev Asset-liability position state - (bytes32 asset, uint amount, bytes32 liability, uint debt)
23
+ bytes4 constant Position = bytes4(keccak256("#position"));
24
24
  /// @dev List wrapper; payload is an embedded repeated block stream
25
25
  bytes4 constant List = bytes4(keccak256("#list"));
26
26
  /// @dev EVM-encoded payload field; layout follows standard ABI tuple encoding
package/codec/Readers.sol CHANGED
@@ -94,6 +94,19 @@ library Readers {
94
94
  }
95
95
  }
96
96
 
97
+ /// @notice Consume a POSITION block and return its fields.
98
+ function unpackPosition(
99
+ Reader memory cur
100
+ ) internal pure returns (bytes32 asset, uint amount, bytes32 liability, uint debt) {
101
+ uint abs = consume(cur, Keys.Position, 128, 128);
102
+ assembly ("memory-safe") {
103
+ asset := mload(abs)
104
+ amount := mload(add(abs, 0x20))
105
+ liability := mload(add(abs, 0x40))
106
+ debt := mload(add(abs, 0x60))
107
+ }
108
+ }
109
+
97
110
  /// @notice Consume a TRANSACTION block and return its fields.
98
111
  /// @param cur Reader; advanced past the block.
99
112
  /// @return from Source account identifier.
package/codec/Schema.sol CHANGED
@@ -43,10 +43,13 @@ pragma solidity ^0.8.33;
43
43
  // - command input and state streams are each a single run of blocks under the
44
44
  // current protocol convention; the block format may support other shapes in
45
45
  // future protocol surfaces
46
- // - `balance(...)` and `custody(...)` are live, linear state in the active command pipeline
46
+ // - `balance(...)`, `custody(...)`, and `position(...)` are live, linear state in the active command pipeline
47
47
  // - pipeline state belongs to the active account while the pipeline is executing
48
48
  // - while a balance or custody is in-flight as pipeline state, it is not simultaneously persisted
49
49
  // in another ledger/store by this protocol
50
+ // - a position pairs live asset and liability sides; commands may transform either side
51
+ // - position state is transient and does not itself create or erase an externally persisted obligation
52
+ // - positions support backward composition, but pipeline steps always execute in encoded order
50
53
  // - commands must preserve, transform, settle, or intentionally consume pipeline state
51
54
  // - input blocks such as `amount(...)`, `allocation(...)`, and `allowance(...)`
52
55
  // express intent, constraints, or references
@@ -90,6 +93,7 @@ library Schemas {
90
93
 
91
94
  // Four-word payloads
92
95
 
96
+ string constant Position = "{ bytes32 asset, uint amount, bytes32 liability, uint debt }";
93
97
  string constant Transaction = "{ bytes32 from, bytes32 to, bytes32 asset, uint amount }";
94
98
  string constant HostAccountAmount = "{ uint host, bytes32 account, bytes32 asset, uint amount }";
95
99