@rootzero/contracts 1.24.0 → 1.25.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 CHANGED
@@ -8,6 +8,45 @@ sections are immutable and must continue to describe the tagged release.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.25.0
12
+
13
+ ### Breaking Changes
14
+
15
+ - Changed `Pipeline.pipe` to accept a scalar `uint` budget and return the
16
+ remaining scalar budget. The standalone mutable `Budget` type remains
17
+ available for other funded execution flows.
18
+ - Split the pipeline hook from its standard implementation. `PipePayablePort`
19
+ now inherits only `PipeHook`; hosts that want the built-in pipeline must
20
+ compose `Pipeline` explicitly, while custom hosts may implement the hook
21
+ without inheriting the standard dispatch and posting implementation.
22
+ - Changed `Decoders.open` to wrap the complete non-empty calldata source as an
23
+ ungrouped cursor. Code that needs the former counted first-homogeneous-run
24
+ behavior must use the new `Decoders.batch` helper.
25
+
26
+ ### Added
27
+
28
+ - Added scalar `Budgets.useValue` and `Budgets.useResourceValue` overloads, and
29
+ `Executions.drainBudget` for transferring an execution budget as a `uint`.
30
+ - Added `PipeHook` to the `Core.sol` and `Endpoints.sol` package barrels.
31
+ - Added `Blocks.expectKey` and key-only `Decoders.consume` and
32
+ `Executions.consume` overloads for validating blocks whose payload shape is
33
+ established by specialized decoding logic.
34
+ - Added `Blocks.require1`, `require2`, `require4`, `require8`, and `require16`,
35
+ completing the fixed-width validation family alongside `require32`.
36
+
37
+ ### Changed
38
+
39
+ - Specialized built-in composite block unpackers to validate their known keys
40
+ directly instead of constructing and evaluating generic specifications.
41
+ - Specialized fixed-width block validation, empty-block inspection, and
42
+ key-only `takeBlock` paths to avoid redundant specification and calldata
43
+ work.
44
+ - Pipeline execution now validates the complete STEP source and rejects a
45
+ trailing block with a different key instead of silently stopping after the
46
+ first homogeneous run.
47
+ - Simplified internal debit-account output allocation to use the fixed-width
48
+ input size directly.
49
+
11
50
  ## 1.24.0
12
51
 
13
52
  ### Breaking Changes
package/Core.sol CHANGED
@@ -14,7 +14,7 @@ import { NativeAsset, Runtime } from "./core/Runtime.sol";
14
14
  import { Admins, CommandHost, Guardians, Host, HostIntroduction, IHostIntroduction } from "./core/Host.sol";
15
15
  import { CommandCalls, FailedCall, NodeCalls, PortCalls, RawNodeCalls } from "./core/Calls.sol";
16
16
  import { EndpointBase, InputEndpointBase } from "./core/Endpoint.sol";
17
- import { Pipeline } from "./core/Pipeline.sol";
17
+ import { PipeHook, Pipeline } from "./core/Pipeline.sol";
18
18
  import { Budget, Budgets } from "./execution/Budget.sol";
19
19
  import { CreditAccountHook, DebitAccountHook, PostHook, RepayHook, SettleHook, Settlement } from "./core/Settlement.sol";
20
20
  import { Portal } from "./core/Portal.sol";
package/Endpoints.sol CHANGED
@@ -6,6 +6,7 @@ pragma solidity ^0.8.33;
6
6
 
7
7
  // Shared endpoint hooks
8
8
  import {Flags} from "./codec/Descriptors.sol";
9
+ import {PipeHook} from "./core/Pipeline.sol";
9
10
  import {CreditAccountHook, DebitAccountHook, PostHook, RepayHook, SettleHook} from "./core/Settlement.sol";
10
11
 
11
12
  // Commands
package/README.md CHANGED
@@ -366,13 +366,15 @@ running the next step. This is the core of `Pipeline.pipe`:
366
366
  ```solidity
367
367
  while (cur.more()) {
368
368
  (uint cmd, uint resources, bytes calldata input) = cur.unpackStep();
369
+ uint128 value;
370
+ (budget, value) = Budgets.useResourceValue(budget, resources);
369
371
  Reader memory transactions;
370
372
  (state, transactions.source) = dispatch(
371
373
  cmd,
372
374
  account,
373
375
  state,
374
376
  input,
375
- budget.useResourceValue(resources)
377
+ value
376
378
  );
377
379
  while (transactions.more()) {
378
380
  (bytes32 from, bytes32 to, bytes32 asset, uint amount) = transactions.unpackTransaction();
@@ -382,6 +384,9 @@ while (cur.more()) {
382
384
  if (state.length != 0) revert UnexpectedState();
383
385
  ```
384
386
 
387
+ `Pipeline.pipe` takes the available native-value budget as a `uint` and returns
388
+ the remaining budget after every step has executed.
389
+
385
390
  A transfer, for instance, is a two-step pipeline: `debitAccount` turns an
386
391
  `#amount` input into `#balance` state, and `payout` consumes that state
387
392
  toward a recipient. Because a pipeline is just blocks, it is also the unit of
@@ -482,11 +487,11 @@ names, access sets, balances — from logs alone, with no artifact files.
482
487
  Import from the package entry points rather than deep paths:
483
488
 
484
489
  - `@rootzero/contracts/Core.sol` — `Host`, access control, `Balances`,
485
- `Settlement`, `Pipeline`, `Portal`, validator
490
+ `Settlement`, `PipeHook`, `Pipeline`, `Portal`, validator
486
491
  - `@rootzero/contracts/Commands.sol` — `CommandBase`, `Execution`, `Flags`,
487
492
  codec helpers, and shared value types for authoring custom commands
488
493
  - `@rootzero/contracts/Endpoints.sol` — command, admin, port, guard, and query
489
- mixins, their hooks, and `Flags`
494
+ mixins, their hooks (including `PipeHook`), and `Flags`
490
495
  - `@rootzero/contracts/Codec.sol` — `Blocks`, calldata `Cur`/`Cursors`, memory
491
496
  `Reader`/`Readers`, `Writers`, `Schemas`, `Descriptors`, `Flags`, `Keys`, and
492
497
  `Specs`
package/codec/Blocks.sol CHANGED
@@ -100,16 +100,33 @@ library Blocks {
100
100
  end = body + len;
101
101
  }
102
102
 
103
+ /// @notice Validate a known block key and return its payload bounds.
104
+ /// @dev DANGER: This performs an unchecked calldata read and validates only
105
+ /// the key. The caller must validate the known payload shape and returned end.
106
+ /// @param abs Absolute calldata position of the header.
107
+ /// @param key Expected block key.
108
+ /// @return body Absolute position of the first payload byte.
109
+ /// @return end Absolute position immediately after the payload.
110
+ function expectKey(uint abs, bytes4 key) internal pure returns (uint body, uint end) {
111
+ uint len = header(abs, key);
112
+ unchecked {
113
+ body = abs + Sizes.Header;
114
+ end = body + len;
115
+ }
116
+ }
117
+
103
118
  /// @dev Validate the key and exact payload size of a fixed-width block.
104
119
  /// @param abs Absolute calldata position of the header.
105
- /// @param spec Expected block specification.
120
+ /// @param key Expected block key.
106
121
  /// @param size Expected payload length.
107
122
  /// @return body Absolute position of the payload.
108
123
  /// @return end Absolute position after the payload.
109
- function expectFixed(uint abs, uint spec, uint size) private pure returns (uint body, uint end) {
110
- if (header(abs, Specs.key(spec)) != size) revert InvalidBlock();
111
- body = abs + Sizes.Header;
112
- end = body + size;
124
+ function expectFixed(uint abs, bytes4 key, uint size) private pure returns (uint body, uint end) {
125
+ if (header(abs, key) != size) revert InvalidBlock();
126
+ unchecked {
127
+ body = abs + Sizes.Header;
128
+ end = body + size;
129
+ }
113
130
  }
114
131
 
115
132
  /// @notice Validate an empty block at an absolute calldata position.
@@ -139,8 +156,9 @@ library Blocks {
139
156
  /// @param key Expected block key.
140
157
  /// @return Whether the expected key occurs with a zero-length payload.
141
158
  function isEmpty(uint abs, uint end, bytes4 key) internal pure returns (bool) {
142
- if (!hasAt(abs, end, key)) return false;
143
- return uint32(uint(read32(abs)) >> 192) == 0;
159
+ if (abs > end || Sizes.Header > end - abs) return false;
160
+ uint head = uint(read32(abs));
161
+ return uint32(head >> 224) == uint32(key) && uint32(head >> 192) == 0;
144
162
  }
145
163
 
146
164
  /// @notice Find the first block with `key` at or after absolute position `abs`.
@@ -1080,6 +1098,46 @@ library Blocks {
1080
1098
  }
1081
1099
  }
1082
1100
 
1101
+ /// @notice Require the byte at an absolute calldata position to match `expected`.
1102
+ /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1103
+ /// @param abs Absolute calldata position.
1104
+ /// @param expected Expected byte.
1105
+ function require1(uint abs, bytes1 expected) internal pure {
1106
+ if (read1(abs) != expected) revert UnexpectedValue();
1107
+ }
1108
+
1109
+ /// @notice Require the two bytes at an absolute calldata position to match `expected`.
1110
+ /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1111
+ /// @param abs Absolute calldata position.
1112
+ /// @param expected Expected two-byte value.
1113
+ function require2(uint abs, bytes2 expected) internal pure {
1114
+ if (read2(abs) != expected) revert UnexpectedValue();
1115
+ }
1116
+
1117
+ /// @notice Require the four bytes at an absolute calldata position to match `expected`.
1118
+ /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1119
+ /// @param abs Absolute calldata position.
1120
+ /// @param expected Expected four-byte value.
1121
+ function require4(uint abs, bytes4 expected) internal pure {
1122
+ if (read4(abs) != expected) revert UnexpectedValue();
1123
+ }
1124
+
1125
+ /// @notice Require the eight bytes at an absolute calldata position to match `expected`.
1126
+ /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1127
+ /// @param abs Absolute calldata position.
1128
+ /// @param expected Expected eight-byte value.
1129
+ function require8(uint abs, bytes8 expected) internal pure {
1130
+ if (read8(abs) != expected) revert UnexpectedValue();
1131
+ }
1132
+
1133
+ /// @notice Require the sixteen bytes at an absolute calldata position to match `expected`.
1134
+ /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1135
+ /// @param abs Absolute calldata position.
1136
+ /// @param expected Expected sixteen-byte value.
1137
+ function require16(uint abs, bytes16 expected) internal pure {
1138
+ if (read16(abs) != expected) revert UnexpectedValue();
1139
+ }
1140
+
1083
1141
  /// @notice Require the word at an absolute calldata position to match `expected`.
1084
1142
  /// @dev DANGER: Unchecked calldata read. Values beyond calldata are zero-padded.
1085
1143
  /// @param abs Absolute calldata position.
@@ -1106,7 +1164,7 @@ library Blocks {
1106
1164
  /// @return a First payload word.
1107
1165
  /// @return end Absolute position after the block.
1108
1166
  function unpack32(uint abs, uint spec) internal pure returns (bytes32 a, uint end) {
1109
- (abs, end) = expectFixed(abs, spec, 32);
1167
+ (abs, end) = expectFixed(abs, Specs.key(spec), 32);
1110
1168
  a = read32(abs);
1111
1169
  }
1112
1170
 
@@ -1117,7 +1175,7 @@ library Blocks {
1117
1175
  /// @return b Second payload word.
1118
1176
  /// @return end Absolute position after the block.
1119
1177
  function unpack64(uint abs, uint spec) internal pure returns (bytes32 a, bytes32 b, uint end) {
1120
- (abs, end) = expectFixed(abs, spec, 64);
1178
+ (abs, end) = expectFixed(abs, Specs.key(spec), 64);
1121
1179
  assembly ("memory-safe") {
1122
1180
  a := calldataload(abs)
1123
1181
  b := calldataload(add(abs, 0x20))
@@ -1135,7 +1193,7 @@ library Blocks {
1135
1193
  uint abs,
1136
1194
  uint spec
1137
1195
  ) internal pure returns (bytes32 a, bytes32 b, bytes32 c, uint end) {
1138
- (abs, end) = expectFixed(abs, spec, 96);
1196
+ (abs, end) = expectFixed(abs, Specs.key(spec), 96);
1139
1197
  assembly ("memory-safe") {
1140
1198
  a := calldataload(abs)
1141
1199
  b := calldataload(add(abs, 0x20))
@@ -1155,7 +1213,7 @@ library Blocks {
1155
1213
  uint abs,
1156
1214
  uint spec
1157
1215
  ) internal pure returns (bytes32 a, bytes32 b, bytes32 c, bytes32 d, uint end) {
1158
- (abs, end) = expectFixed(abs, spec, 128);
1216
+ (abs, end) = expectFixed(abs, Specs.key(spec), 128);
1159
1217
  assembly ("memory-safe") {
1160
1218
  a := calldataload(abs)
1161
1219
  b := calldataload(add(abs, 0x20))
@@ -1177,7 +1235,7 @@ library Blocks {
1177
1235
  uint abs,
1178
1236
  uint spec
1179
1237
  ) internal pure returns (bytes32 a, bytes32 b, bytes32 c, bytes32 d, bytes32 e, uint end) {
1180
- (abs, end) = expectFixed(abs, spec, 160);
1238
+ (abs, end) = expectFixed(abs, Specs.key(spec), 160);
1181
1239
  assembly ("memory-safe") {
1182
1240
  a := calldataload(abs)
1183
1241
  b := calldataload(add(abs, 0x20))
@@ -1653,7 +1711,7 @@ library Blocks {
1653
1711
  uint abs
1654
1712
  ) internal pure returns (uint entity, bytes calldata stream, uint end) {
1655
1713
  uint limit;
1656
- (abs, limit) = expect(abs, Specs.Annotation);
1714
+ (abs, limit) = expectKey(abs, Keys.Annotation);
1657
1715
  assembly ("memory-safe") {
1658
1716
  entity := calldataload(abs)
1659
1717
  }
@@ -1671,7 +1729,7 @@ library Blocks {
1671
1729
  uint abs
1672
1730
  ) internal pure returns (bytes32 account, bytes calldata state, bytes calldata input, uint end) {
1673
1731
  uint limit;
1674
- (abs, limit) = expect(abs, Specs.Context);
1732
+ (abs, limit) = expectKey(abs, Keys.Context);
1675
1733
  assembly ("memory-safe") {
1676
1734
  account := calldataload(abs)
1677
1735
  }
@@ -1692,7 +1750,7 @@ library Blocks {
1692
1750
  uint abs
1693
1751
  ) internal pure returns (uint cmd, uint resources, bytes calldata input, uint end) {
1694
1752
  uint limit;
1695
- (abs, limit) = expect(abs, Specs.Step);
1753
+ (abs, limit) = expectKey(abs, Keys.Step);
1696
1754
  assembly ("memory-safe") {
1697
1755
  cmd := calldataload(abs)
1698
1756
  resources := calldataload(add(abs, 0x20))
@@ -1711,7 +1769,7 @@ library Blocks {
1711
1769
  uint abs
1712
1770
  ) internal pure returns (uint target, uint resources, bytes calldata payload, uint end) {
1713
1771
  uint limit;
1714
- (abs, limit) = expect(abs, Specs.Call);
1772
+ (abs, limit) = expectKey(abs, Keys.Call);
1715
1773
  assembly ("memory-safe") {
1716
1774
  target := calldataload(abs)
1717
1775
  resources := calldataload(add(abs, 0x20))
@@ -1730,7 +1788,7 @@ library Blocks {
1730
1788
  uint abs
1731
1789
  ) internal pure returns (uint portal, uint resources, bytes calldata input, uint end) {
1732
1790
  uint limit;
1733
- (abs, limit) = expect(abs, Specs.Relay);
1791
+ (abs, limit) = expectKey(abs, Keys.Relay);
1734
1792
  assembly ("memory-safe") {
1735
1793
  portal := calldataload(abs)
1736
1794
  resources := calldataload(add(abs, 0x20))
@@ -1749,7 +1807,7 @@ library Blocks {
1749
1807
  uint abs
1750
1808
  ) internal pure returns (uint portal, uint resources, bytes calldata payload, uint end) {
1751
1809
  uint limit;
1752
- (abs, limit) = expect(abs, Specs.Dispatch);
1810
+ (abs, limit) = expectKey(abs, Keys.Dispatch);
1753
1811
  assembly ("memory-safe") {
1754
1812
  portal := calldataload(abs)
1755
1813
  resources := calldataload(add(abs, 0x20))
@@ -1765,7 +1823,7 @@ library Blocks {
1765
1823
  /// @return end Absolute position after the block.
1766
1824
  function unpackLabel(uint abs) internal pure returns (bytes32 namespace, string memory name, uint end) {
1767
1825
  uint limit;
1768
- (abs, limit) = expect(abs, Specs.Label);
1826
+ (abs, limit) = expectKey(abs, Keys.Label);
1769
1827
  assembly ("memory-safe") {
1770
1828
  namespace := calldataload(abs)
1771
1829
  }
@@ -1783,7 +1841,7 @@ library Blocks {
1783
1841
  /// @return end Absolute position after the block.
1784
1842
  function unpackSchema(uint abs) internal pure returns (uint spec, string memory body, bytes32 name, uint end) {
1785
1843
  uint limit;
1786
- (abs, limit) = expect(abs, Specs.Schema);
1844
+ (abs, limit) = expectKey(abs, Keys.Schema);
1787
1845
  assembly ("memory-safe") {
1788
1846
  spec := calldataload(abs)
1789
1847
  }
@@ -1810,7 +1868,7 @@ library Blocks {
1810
1868
  uint abs
1811
1869
  ) internal pure returns (uint handler, uint resources, bytes32 key, bytes calldata witness, uint end) {
1812
1870
  uint limit;
1813
- (abs, limit) = expect(abs, Specs.Recover);
1871
+ (abs, limit) = expectKey(abs, Keys.Recover);
1814
1872
  assembly ("memory-safe") {
1815
1873
  handler := calldataload(abs)
1816
1874
  resources := calldataload(add(abs, 0x20))
@@ -32,10 +32,18 @@ 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`.
36
- /// @param source Calldata block stream to open.
37
- /// @return cur Cursor spanning the first homogeneous run.
35
+ /// @notice Open a non-empty calldata source as an ungrouped cursor.
36
+ /// @param source Calldata region to open.
37
+ /// @return cur Cursor spanning the complete source.
38
38
  function open(bytes calldata source) internal pure returns (Cur memory cur) {
39
+ if (source.length == 0) revert Blocks.EmptyRun();
40
+ cur.state = Cursors.wrap(source, 0, 0);
41
+ }
42
+
43
+ /// @notice Open the first homogeneous batch in `source`.
44
+ /// @param source Calldata block stream to open.
45
+ /// @return cur Counted cursor spanning the first homogeneous batch.
46
+ function batch(bytes calldata source) internal pure returns (Cur memory cur) {
39
47
  (uint abs, uint limit) = Cursors.bounds(source);
40
48
  if (abs == limit) revert Blocks.EmptyRun();
41
49
 
@@ -69,6 +77,18 @@ library Decoders {
69
77
  cur.state = cur.state.seekAbs(end);
70
78
  }
71
79
 
80
+ /// @notice Validate a known key and consume the next block from a cursor.
81
+ /// @dev Validates no payload-size constraint beyond proving the complete block
82
+ /// lies within the cursor's logical region.
83
+ /// @param cur Cursor advanced over the complete block.
84
+ /// @param key Expected block key.
85
+ /// @return abs Absolute position of the first payload byte.
86
+ /// @return end Absolute position immediately after the payload.
87
+ function consume(Cur memory cur, bytes4 key) internal pure returns (uint abs, uint end) {
88
+ (abs, end) = Blocks.expectKey(cur.state.absolute(), key);
89
+ cur.state = cur.state.seekAbs(end);
90
+ }
91
+
72
92
  /// @notice Consume a matching empty block from a cursor when present.
73
93
  /// @param cur Cursor advanced only when the matching block is empty.
74
94
  /// @param key Expected block key.
@@ -268,7 +288,7 @@ library Decoders {
268
288
  /// @return out Cursor spanning the complete encoded block.
269
289
  function takeBlock(Cur memory cur, bytes4 key) internal pure returns (Cur memory out) {
270
290
  uint abs = cur.state.absolute();
271
- (, uint end) = consume(cur, Specs.create(key, 0, 0, 0));
291
+ (, uint end) = consume(cur, key);
272
292
  out.state = Cursors.create(abs, end - abs, 0, 0, 0);
273
293
  }
274
294
 
package/commands/Base.sol CHANGED
@@ -99,10 +99,7 @@ abstract contract CommandBase is CallerAccess, EndpointBase, ReceivedEvent {
99
99
  uint descriptor,
100
100
  uint batches
101
101
  ) internal view returns (Execution memory exec) {
102
- bytes32 account;
103
- bytes calldata state;
104
- bytes calldata input;
105
- (account, state, input) = unpackCommandContext(context);
102
+ (bytes32 account, bytes calldata state, bytes calldata input) = unpackCommandContext(context);
106
103
  exec = Executions.open(state, input, descriptor, batches);
107
104
  exec.account = account;
108
105
  }
@@ -67,11 +67,8 @@ abstract contract DebitAccountInternal is DebitAccount {
67
67
  ) internal returns (bytes memory, bytes memory) {
68
68
  if (value != 0) revert ValueNotAllowed();
69
69
  if (state.length != 0) revert Executions.ZeroStride();
70
- if (input.length == 0) revert Blocks.EmptyRun();
71
-
72
- Cur memory cur = Decoders.wrap(input);
73
- uint count = input.length / Sizes.Amount;
74
- bytes memory output = new bytes(count * Sizes.Balance);
70
+ Cur memory cur = Decoders.open(input);
71
+ bytes memory output = new bytes(input.length);
75
72
  uint i;
76
73
 
77
74
  while (cur.more()) {
@@ -13,10 +13,7 @@ abstract contract AdminBase is NodeAccess, CommandBase {
13
13
  uint descriptor,
14
14
  uint batches
15
15
  ) internal view returns (Execution memory exec) {
16
- bytes32 account;
17
- bytes calldata state;
18
- bytes calldata input;
19
- (account, state, input) = unpackCommandContext(context);
16
+ (bytes32 account, bytes calldata state, bytes calldata input) = unpackCommandContext(context);
20
17
  enforceAdmin(account, msg.sender);
21
18
  exec = Executions.open(state, input, descriptor, batches);
22
19
  exec.account = account;
package/core/Pipeline.sol CHANGED
@@ -3,15 +3,25 @@ pragma solidity ^0.8.33;
3
3
 
4
4
  import {Decoders, Cur, Readers, Reader} from "../Codec.sol";
5
5
  import {PostHook} from "./Settlement.sol";
6
- import {Budget, Budgets} from "../execution/Budget.sol";
6
+ import {Budgets} from "../execution/Budget.sol";
7
7
 
8
8
  using Decoders for Cur;
9
9
  using Readers for Reader;
10
- using Budgets for Budget;
10
+
11
+ /// @notice Hook implemented by hosts that execute encoded step streams.
12
+ abstract contract PipeHook {
13
+ /// @notice Execute a step stream and return its remaining native-value budget.
14
+ function pipe(
15
+ bytes32 account,
16
+ bytes memory state,
17
+ bytes calldata steps,
18
+ uint budget
19
+ ) internal virtual returns (uint remaining);
20
+ }
11
21
 
12
22
  /// @title Pipeline
13
23
  /// @notice Core pipeline functionality shared by higher-level surfaces.
14
- abstract contract Pipeline is PostHook {
24
+ abstract contract Pipeline is PipeHook, PostHook {
15
25
  /// @dev Thrown when the pipeline finishes with non-empty threaded state.
16
26
  error UnexpectedState();
17
27
 
@@ -36,18 +46,26 @@ abstract contract Pipeline is PostHook {
36
46
 
37
47
  /// @notice Execute a STEP block stream through the pipeline.
38
48
  /// @dev Reverts with `UnexpectedState` if the final threaded state is non-empty.
39
- /// Callers remain responsible for settling any unspent value in `budget`.
49
+ /// Callers remain responsible for settling the returned unspent value.
40
50
  /// @param account Account identifier used for each dispatched step.
41
51
  /// @param state Initial state block stream passed to the first step.
42
52
  /// @param steps STEP block stream to execute.
43
- /// @param budget Mutable native-value budget shared across all steps.
44
- function pipe(bytes32 account, bytes memory state, bytes calldata steps, Budget memory budget) internal {
53
+ /// @param budget Native-value budget shared across all steps.
54
+ /// @return remaining Native value remaining after every step executes.
55
+ function pipe(
56
+ bytes32 account,
57
+ bytes memory state,
58
+ bytes calldata steps,
59
+ uint budget
60
+ ) internal virtual override returns (uint remaining) {
45
61
  Cur memory cur = Decoders.open(steps);
46
62
 
47
63
  while (cur.more()) {
48
64
  (uint cmd, uint resources, bytes calldata input) = cur.unpackStep();
65
+ uint128 value;
66
+ (budget, value) = Budgets.useResourceValue(budget, resources);
49
67
  Reader memory txs;
50
- (state, txs.source) = dispatch(cmd, account, state, input, budget.useResourceValue(resources));
68
+ (state, txs.source) = dispatch(cmd, account, state, input, value);
51
69
 
52
70
  while (txs.more()) {
53
71
  (bytes32 from, bytes32 to, bytes32 asset, uint amount) = txs.unpackTransaction();
@@ -56,5 +74,6 @@ abstract contract Pipeline is PostHook {
56
74
  }
57
75
 
58
76
  if (state.length != 0) revert UnexpectedState();
77
+ return budget;
59
78
  }
60
79
  }
@@ -29,6 +29,15 @@ library Budgets {
29
29
  return value;
30
30
  }
31
31
 
32
+ /// @notice Deduct an exact native value from a scalar budget.
33
+ /// @param budget Remaining native value in wei.
34
+ /// @param value Native value to consume in wei.
35
+ /// @return remaining Native value remaining after the deduction.
36
+ function useValue(uint budget, uint value) internal pure returns (uint remaining) {
37
+ if (value > budget) revert InsufficientValue();
38
+ remaining = budget - value;
39
+ }
40
+
32
41
  /// @notice Deduct the EVM value lane of `resources` from `budget`.
33
42
  /// @dev EVM resources use the low 128 bits as native value/endowment.
34
43
  /// @param budget Mutable budget to debit.
@@ -38,6 +47,19 @@ library Budgets {
38
47
  return uint128(useValue(budget, uint128(resources)));
39
48
  }
40
49
 
50
+ /// @notice Deduct the EVM value lane of `resources` from a scalar budget.
51
+ /// @param budget Remaining native value in wei.
52
+ /// @param resources Packed resources whose low 128 bits contain native value.
53
+ /// @return remaining Native value remaining after the deduction.
54
+ /// @return value Native value consumed from the budget.
55
+ function useResourceValue(
56
+ uint budget,
57
+ uint resources
58
+ ) internal pure returns (uint remaining, uint128 value) {
59
+ value = uint128(resources);
60
+ remaining = useValue(budget, value);
61
+ }
62
+
41
63
  /// @notice Remove and return all remaining value from `budget`.
42
64
  /// @param budget Mutable budget to drain.
43
65
  /// @return value Native value removed from the budget.
@@ -246,6 +246,20 @@ library Executions {
246
246
  exec.decoders = cur.seekAbs(end);
247
247
  }
248
248
 
249
+ /// @notice Validate a known key and consume the next block from an execution decoder lane.
250
+ /// @dev Validates no payload-size constraint beyond proving the complete block
251
+ /// lies within the selected lane's logical region.
252
+ /// @param exec Execution whose selected decoder cursor is advanced over the complete block.
253
+ /// @param lane Execution decoder lane to select.
254
+ /// @param key Expected block key.
255
+ /// @return abs Absolute position of the first payload byte.
256
+ /// @return end Absolute position immediately after the payload.
257
+ function consume(Execution memory exec, uint8 lane, bytes4 key) internal pure returns (uint abs, uint end) {
258
+ uint cur = exec.decoders.select(lane);
259
+ (abs, end) = Blocks.expectKey(cur.absolute(), key);
260
+ exec.decoders = cur.seekAbs(end);
261
+ }
262
+
249
263
  /// @notice Validate and consume one block from a decoder lane, returning its complete encoding.
250
264
  /// @param exec Execution whose selected decoder cursor is advanced past the block.
251
265
  /// @param lane Execution decoder lane to select.
@@ -256,7 +270,7 @@ library Executions {
256
270
  uint8 lane,
257
271
  bytes4 key
258
272
  ) internal pure returns (bytes calldata data) {
259
- (uint abs, uint end) = consume(exec, lane, Specs.create(key, 0, 0, 0));
273
+ (uint abs, uint end) = consume(exec, lane, key);
260
274
  data = msg.data[abs - Sizes.Header:end];
261
275
  }
262
276
 
@@ -1403,13 +1417,20 @@ library Executions {
1403
1417
  // Value and transaction writing
1404
1418
  // -------------------------------------------------------------------------
1405
1419
 
1420
+ /// @notice Remove and return the remaining execution value budget.
1421
+ /// @param exec Execution whose budget is drained.
1422
+ /// @return budget Native value removed from the execution.
1423
+ function drainBudget(Execution memory exec) internal pure returns (uint budget) {
1424
+ budget = exec.budget;
1425
+ exec.budget = 0;
1426
+ }
1427
+
1406
1428
  /// @notice Transfer the remaining value budget out of an execution.
1407
1429
  /// @dev Clears `exec.budget` so the returned budget becomes its sole owner.
1408
1430
  /// @param exec Execution whose budget is detached.
1409
1431
  /// @return budget Detached budget containing the remaining value.
1410
1432
  function takeBudget(Execution memory exec) internal pure returns (Budget memory budget) {
1411
- budget.remaining = exec.budget;
1412
- exec.budget = 0;
1433
+ budget.remaining = drainBudget(exec);
1413
1434
  }
1414
1435
 
1415
1436
  /// @notice Deduct an exact native value from the execution budget.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootzero/contracts",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Solidity contracts and protocol building blocks for rootzero hosts and commands.",
5
5
  "private": false,
6
6
  "license": "GPL-3.0-only",
package/ports/Pipe.sol CHANGED
@@ -3,17 +3,16 @@ pragma solidity ^0.8.33;
3
3
 
4
4
  import {PortBase} from "./Base.sol";
5
5
  import {Flags} from "../codec/Descriptors.sol";
6
- import {Pipeline} from "../core/Pipeline.sol";
6
+ import {PipeHook} from "../core/Pipeline.sol";
7
7
  import {Specs} from "../Codec.sol";
8
8
  import {Execution, Executions, Lanes} from "../execution/Execution.sol";
9
- import {Budget} from "../execution/Budget.sol";
10
9
 
11
10
  using Executions for Execution;
12
11
 
13
12
  /// @title PipePayablePort
14
13
  /// @notice Port that consumes CONTEXT blocks and executes each input as a step stream.
15
14
  /// Each context's input bytes are passed to the shared pipeline.
16
- abstract contract PipePayablePort is PortBase, Pipeline {
15
+ abstract contract PipePayablePort is PortBase, PipeHook {
17
16
  uint private immutable descriptor;
18
17
 
19
18
  constructor() {
@@ -27,11 +26,11 @@ abstract contract PipePayablePort is PortBase, Pipeline {
27
26
  /// @return Empty response bytes.
28
27
  function portPipePayable(bytes calldata data) external payable onlyPeer returns (bytes memory) {
29
28
  Execution memory exec = openInput(data, descriptor, 0);
30
- Budget memory budget = exec.takeBudget();
29
+ uint budget = exec.drainBudget();
31
30
 
32
31
  while (exec.more()) {
33
32
  (bytes32 account, bytes calldata state, bytes calldata input) = exec.unpackContext(Lanes.Input);
34
- pipe(account, state, input, budget);
33
+ budget = pipe(account, state, input, budget);
35
34
  }
36
35
 
37
36
  return "";