@rootzero/contracts 1.27.0 → 1.28.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,30 @@ sections are immutable and must continue to describe the tagged release.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 1.28.0
12
+
13
+ ### Breaking Changes
14
+
15
+ - Expanded STEP's native `value` field from `uint128` to `uint`, increasing an
16
+ empty STEP block from 64 to 80 bytes. Plain native values now use `uint`
17
+ throughout runtime APIs; packed `resources` remain separate `uint` words
18
+ whose EVM value lane is extracted explicitly with `useResourceValue`.
19
+ - Replaced the `CommandCalls` abstraction and its allocating `encodeCommandCall`
20
+ and `callCommand` helpers with the free `rawCommandCall` function. The caller
21
+ now supplies the decoded selector and target after validating and authorizing
22
+ the command, then uses the single-scratch assembly call path.
23
+ - Added the required `ExecuteHook` to `Pipeline` and removed its `dispatch` hook.
24
+ Steps whose command IDs target the current host use `execute`; other commands
25
+ are checked through `TrustAccess` and called directly with `rawCommandCall`.
26
+
27
+ ### Changed
28
+
29
+ - Added the free `unpackCommand` utility for validating a command node and
30
+ extracting its selector and target in one operation. Pipeline steps now use
31
+ it to validate command IDs and select local execution by target address.
32
+ - Simplified `NodeAccess.ensureTrusted` to a single mapping check; an unset zero
33
+ node already resolves to false like every other untrusted node.
34
+
11
35
  ## 1.27.0
12
36
 
13
37
  ### Breaking Changes
package/Core.sol CHANGED
@@ -12,9 +12,9 @@ import { Balances, InsufficientFunds } from "./core/Balances.sol";
12
12
  import { Escrows, InsufficientEscrow } from "./core/Escrows.sol";
13
13
  import { NativeAsset, Runtime } from "./core/Runtime.sol";
14
14
  import { Admins, CommandHost, Guardians, Host, HostIntroduction, IHostIntroduction } from "./core/Host.sol";
15
- import { CommandCalls, FailedCall, NodeCalls, PortCalls, RawNodeCalls } from "./core/Calls.sol";
15
+ import { FailedCall, NodeCalls, PortCalls, RawNodeCalls, rawCommandCall } from "./core/Calls.sol";
16
16
  import { EndpointBase, InputEndpointBase } from "./core/Endpoint.sol";
17
- import { PipeHook, Pipeline } from "./core/Pipeline.sol";
17
+ import { ExecuteHook, 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,7 +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
+ import {ExecuteHook, PipeHook} from "./core/Pipeline.sol";
10
10
  import {CreditAccountHook, DebitAccountHook, PostHook, RepayHook, SettleHook} from "./core/Settlement.sol";
11
11
 
12
12
  // Commands
package/README.md CHANGED
@@ -365,7 +365,7 @@ A single command is rarely the whole story. A pipeline is a run of `#step`
365
365
  blocks executed in order within one transaction:
366
366
 
367
367
  ```txt
368
- step { uint cmd, uint128 value, #bytes as input }
368
+ step { uint cmd, uint value, #bytes as input }
369
369
  ```
370
370
 
371
371
  Each step names a command, the native value it may spend, and its input.
@@ -379,22 +379,22 @@ debits each asset through the standard account hook, introduces matching
379
379
  native asset through the same hook. Its pipeline-local implementation uses assigned step value first when
380
380
  bootstrapping the native asset, debits any remainder from the account, and
381
381
  returns unused assigned value as credit. Bootstrap is registered with command
382
- metadata but is only executable through local pipeline dispatch. This is the core of
382
+ metadata but is only executable through local pipeline execution. This is the core of
383
383
  `Pipeline.pipe`:
384
384
 
385
385
  ```solidity
386
386
  while (cur.more()) {
387
- (uint cmd, uint128 value, bytes calldata input) = cur.unpackStep();
387
+ (uint cmd, uint value, bytes calldata input) = cur.unpackStep();
388
+ (bytes4 selector, address target) = unpackCommand(cmd);
388
389
  if (budget < value) revert InsufficientValue();
389
390
  unchecked { budget -= value; }
390
391
  uint credit;
391
- (state, credit) = dispatch(
392
- cmd,
393
- account,
394
- state,
395
- input,
396
- value
397
- );
392
+ if (target == address(this)) {
393
+ (state, credit) = execute(cmd, account, state, input, value);
394
+ } else {
395
+ ensureTrusted(cmd);
396
+ (state, credit) = rawCommandCall(selector, target, value, account, state, input);
397
+ }
398
398
  budget += credit;
399
399
  }
400
400
  if (state.length != 0) revert UnexpectedState();
@@ -407,9 +407,11 @@ settles that final value once.
407
407
  A transfer, for instance, is a two-step pipeline: `debitAccount` turns an
408
408
  `#amount` input into `#balance` state, and `payout` consumes that state
409
409
  toward a recipient. Because a pipeline is just blocks, it is also the unit of
410
- command batching. A step's `uint128 value` is drawn directly from the shared
411
- native-value budget. Transport envelopes retain separate chain-specific
412
- `resources` fields for adapters that also need gas or runtime parameters.
410
+ command batching. A step's plain `uint value` is drawn directly from the shared
411
+ native-value budget. Transport envelopes retain separate opaque, packed
412
+ chain-specific `resources` fields for adapters that also need gas or runtime
413
+ parameters. A `resources` word is never itself native value; EVM adapters use
414
+ `useResourceValue` to extract its low 128-bit value lane before spending it.
413
415
 
414
416
  Hosts that implement a pipeline locally can inherit `Bootstrap`,
415
417
  `CashoutInternal`, `DebitAccountInternal`, `CreditAccountInternal`,
@@ -509,11 +511,11 @@ names, access sets, balances — from logs alone, with no artifact files.
509
511
  Import from the package entry points rather than deep paths:
510
512
 
511
513
  - `@rootzero/contracts/Core.sol` — `Host`, access control, `Balances`,
512
- `Settlement`, `PipeHook`, `Pipeline`, `Portal`, validator
514
+ `Settlement`, `ExecuteHook`, `PipeHook`, `Pipeline`, `Portal`, validator
513
515
  - `@rootzero/contracts/Commands.sol` — `CommandBase`, `Execution`, `Flags`,
514
516
  codec helpers, and shared value types for authoring custom commands
515
517
  - `@rootzero/contracts/Endpoints.sol` — command, admin, port, guard, and query
516
- mixins, their hooks (including `PipeHook`), and `Flags`
518
+ mixins, their hooks (including `ExecuteHook` and `PipeHook`), and `Flags`
517
519
  - `@rootzero/contracts/Codec.sol` — `Blocks`, calldata `Cur`/`Cursors`, memory
518
520
  `Memory`, `Writers`, `Schemas`, `Descriptors`, `Flags`, `Keys`, and
519
521
  `Specs`
package/Utils.sol CHANGED
@@ -10,7 +10,7 @@ import { Actions } from "./utils/Actions.sol";
10
10
  import { Amounts, Assets } from "./utils/Assets.sol";
11
11
  import { ECDSA } from "./utils/ECDSA.sol";
12
12
  import { Ids } from "./utils/Ids.sol";
13
- import { Nodes } from "./utils/Nodes.sol";
13
+ import { Nodes, unpackCommand } from "./utils/Nodes.sol";
14
14
  import { Layout } from "./utils/Layout.sol";
15
15
  import { BadAmount, InsufficientValue, InvalidAccount, InvalidAsset, InvalidContract, InvalidId, InvalidPreimage, MissingCursor, NotDivisible, OutOfBounds, UnauthorizedAsset, UnconsumedData, UnexpectedPosition, ValueOverflow, ZeroAddress, ZeroAmount} from "./utils/Errors.sol";
16
16
  import { addrOr, applyBps, beforeBps, bytes32ToInt, bytes32ToString, clear8, clear16, clear32, clear64, divisible, ensureAddr, ensureContract, hash32, intToBytes32, isFamily, matchesBase, MAX_BPS, max8, max16, max24, max32, max40, max64, max96, max128, max160, replace8, replace16, replace32, replace64, retryTicket, toLocalBase, toUnspecifiedBase } from "./utils/Utils.sol";
package/codec/Blocks.sol CHANGED
@@ -761,17 +761,17 @@ library Blocks {
761
761
  /// @param cmd Command identifier.
762
762
  /// @param value Native value assigned to the step.
763
763
  /// @param input Command input.
764
- function writeStep(bytes memory dst, uint i, uint cmd, uint128 value, bytes memory input) internal pure {
765
- uint len = 48 + Sizes.Header + input.length;
764
+ function writeStep(bytes memory dst, uint i, uint cmd, uint value, bytes memory input) internal pure {
765
+ uint len = 64 + Sizes.Header + input.length;
766
766
  uint key = uint32(Keys.Step);
767
767
  uint byteskey = uint32(Keys.Bytes);
768
768
  assembly ("memory-safe") {
769
769
  let p := add(add(dst, 0x20), i)
770
770
  mstore(p, or(shl(224, key), shl(192, len)))
771
771
  mstore(add(p, 0x08), cmd)
772
- mstore(add(p, 0x28), shl(128, value))
772
+ mstore(add(p, 0x28), value)
773
773
 
774
- let q := add(p, 0x38)
774
+ let q := add(p, 0x48)
775
775
  let inputlen := mload(input)
776
776
  mstore(q, or(shl(224, byteskey), shl(192, inputlen)))
777
777
  mcopy(add(q, 0x08), add(input, 0x20), inputlen)
@@ -1031,17 +1031,17 @@ library Blocks {
1031
1031
  }
1032
1032
 
1033
1033
  /// @notice Encode a STEP block at `i`, copying its nested input from calldata.
1034
- function copyStep(bytes memory dst, uint i, uint cmd, uint128 value, bytes calldata input) internal pure {
1035
- uint len = max32(48 + Sizes.Header + input.length);
1034
+ function copyStep(bytes memory dst, uint i, uint cmd, uint value, bytes calldata input) internal pure {
1035
+ uint len = max32(64 + Sizes.Header + input.length);
1036
1036
  uint key = uint32(Keys.Step);
1037
1037
  uint byteskey = uint32(Keys.Bytes);
1038
1038
  assembly ("memory-safe") {
1039
1039
  let p := add(add(dst, 0x20), i)
1040
1040
  mstore(p, or(shl(224, key), shl(192, len)))
1041
1041
  mstore(add(p, 0x08), cmd)
1042
- mstore(add(p, 0x28), shl(128, value))
1042
+ mstore(add(p, 0x28), value)
1043
1043
 
1044
- let q := add(p, 0x38)
1044
+ let q := add(p, 0x48)
1045
1045
  let inputlen := input.length
1046
1046
  mstore(q, or(shl(224, byteskey), shl(192, inputlen)))
1047
1047
  calldatacopy(add(q, 0x08), input.offset, inputlen)
@@ -1836,14 +1836,14 @@ library Blocks {
1836
1836
  /// @return end Absolute position after the block.
1837
1837
  function unpackStep(
1838
1838
  uint abs
1839
- ) internal pure returns (uint cmd, uint128 value, bytes calldata input, uint end) {
1839
+ ) internal pure returns (uint cmd, uint value, bytes calldata input, uint end) {
1840
1840
  uint limit;
1841
1841
  (abs, limit) = expectKey(abs, Keys.Step);
1842
1842
  assembly ("memory-safe") {
1843
1843
  cmd := calldataload(abs)
1844
- value := shr(128, calldataload(add(abs, 0x20)))
1844
+ value := calldataload(add(abs, 0x20))
1845
1845
  }
1846
- (input, end) = unpackBytes(abs + 48);
1846
+ (input, end) = unpackBytes(abs + 64);
1847
1847
  if (end != limit) revert InvalidBlock();
1848
1848
  }
1849
1849
 
@@ -2203,14 +2203,14 @@ library Blocks {
2203
2203
  /// @param value Native value assigned to the step.
2204
2204
  /// @param input Raw nested input payload.
2205
2205
  /// @return encoded Encoded STEP block bytes.
2206
- function createStep(uint cmd, uint128 value, bytes memory input) internal pure returns (bytes memory encoded) {
2206
+ function createStep(uint cmd, uint value, bytes memory input) internal pure returns (bytes memory encoded) {
2207
2207
  uint len = max32(Sizes.Step + input.length);
2208
2208
  encoded = allocate(len);
2209
2209
  writeStep(encoded, 0, cmd, value, input);
2210
2210
  }
2211
2211
 
2212
2212
  /// @notice Encode a STEP block by copying its nested input from calldata.
2213
- function createStepCopy(uint cmd, uint128 value, bytes calldata input) internal pure returns (bytes memory encoded) {
2213
+ function createStepCopy(uint cmd, uint value, bytes calldata input) internal pure returns (bytes memory encoded) {
2214
2214
  uint len = max32(Sizes.Step + input.length);
2215
2215
  encoded = allocate(len);
2216
2216
  copyStep(encoded, 0, cmd, value, input);
@@ -740,7 +740,7 @@ library Decoders {
740
740
  /// @return input Decoded command input.
741
741
  function unpackStep(
742
742
  Cur memory cur
743
- ) internal pure returns (uint cmd, uint128 value, bytes calldata input) {
743
+ ) internal pure returns (uint cmd, uint value, bytes calldata input) {
744
744
  uint abs = cur.state.absolute();
745
745
  uint end;
746
746
  (cmd, value, input, end) = Blocks.unpackStep(abs);
package/codec/Keys.sol CHANGED
@@ -39,7 +39,7 @@ library Keys {
39
39
  bytes4 constant Account = bytes4(keccak256("#account"));
40
40
  /// @dev Transfer record passed through the pipeline - (bytes32 from, bytes32 to, bytes32 asset, uint amount)
41
41
  bytes4 constant Transaction = bytes4(keccak256("#transaction"));
42
- /// @dev Sub-command invocation - (uint cmd, uint128 value, #bytes as input)
42
+ /// @dev Sub-command invocation - (uint cmd, uint value, #bytes as input)
43
43
  bytes4 constant Step = bytes4(keccak256("#step"));
44
44
  /// @dev Portal relay input - (uint portal, uint resources, #bytes as input)
45
45
  bytes4 constant Relay = bytes4(keccak256("#relay"));
package/codec/Schema.sol CHANGED
@@ -29,10 +29,11 @@ pragma solidity ^0.8.33;
29
29
  // - `portal` fields identify destination portal hosts. By convention the value
30
30
  // is the portal implementation's host ID; core passes it through unchanged
31
31
  // and hooks may validate or resolve it for their transport
32
- // - `resources` fields are chain-specific resource words. A portal adapter
33
- // interprets them for the destination runtime. EVM resources use the low
34
- // 128 bits as native value.
35
- // - STEP encodes native `value` directly as uint128; it does not carry a
32
+ // - `resources` fields are opaque chain-specific packed words, not native
33
+ // values. A portal adapter interprets them for the destination runtime. EVM
34
+ // resources use the low 128 bits as native value, extracted explicitly with
35
+ // `useResourceValue` before spending.
36
+ // - STEP encodes native `value` directly as uint; it does not carry a
36
37
  // chain-specific resources word
37
38
  // - dotted field names and aliases, e.g. `dst.portal` or `#bytes as dst.payload`,
38
39
  // are offchain projection metadata only and do not change runtime encoding
@@ -116,7 +117,7 @@ library Schemas {
116
117
 
117
118
  // Composite payloads
118
119
 
119
- string constant Step = "uint cmd, uint128 value, #bytes as input";
120
+ string constant Step = "uint cmd, uint value, #bytes as input";
120
121
  string constant Call = "uint target, uint resources, #bytes as payload";
121
122
  string constant Relay = "uint portal, uint resources, #bytes as input";
122
123
  string constant Dispatch = "uint portal, uint resources, #bytes as payload";
package/codec/Specs.sol CHANGED
@@ -21,8 +21,8 @@ library Sizes {
21
21
  uint constant B128 = Header + 4 * Word;
22
22
  /// @dev 8 header + 160 payload = 168 bytes total.
23
23
  uint constant B160 = Header + 5 * Word;
24
- /// @dev Minimum STEP size: 8 header + 32 command + 16 value + 8 nested BYTES header.
25
- uint constant Step = 2 * Header + Word + 16;
24
+ /// @dev Minimum STEP size: 8 header + 32 command + 32 value + 8 nested BYTES header.
25
+ uint constant Step = 2 * Header + 2 * Word;
26
26
  /// @dev STATUS block: 8 header + 32 status code = 40 bytes
27
27
  uint constant Status = B32;
28
28
  /// @dev CASHOUT block: 8 header + 32 native-asset amount = 40 bytes
@@ -64,7 +64,6 @@ library Specs {
64
64
  uint private constant Exact128 = 128 * SizeFields;
65
65
  uint private constant UnboundedHint128 = uint(128) << 136;
66
66
  uint private constant UnboundedMin40Hint256 = (uint(40) << 192) | (uint(256) << 136);
67
- uint private constant UnboundedMin56Hint256 = (uint(56) << 192) | (uint(256) << 136);
68
67
  uint private constant UnboundedMin72Hint256 = (uint(72) << 192) | (uint(256) << 136);
69
68
  uint private constant UnboundedMin48Hint512 = (uint(48) << 192) | (uint(512) << 136);
70
69
  uint private constant UnboundedMin104Hint256 = (uint(104) << 192) | (uint(256) << 136);
@@ -85,7 +84,7 @@ library Specs {
85
84
  uint constant String = uint(bytes32(Keys.String)) | UnboundedHint128;
86
85
  uint constant Account = uint(bytes32(Keys.Account)) | Exact32;
87
86
  uint constant Transaction = uint(bytes32(Keys.Transaction)) | Exact128;
88
- uint constant Step = uint(bytes32(Keys.Step)) | UnboundedMin56Hint256;
87
+ uint constant Step = uint(bytes32(Keys.Step)) | UnboundedMin72Hint256;
89
88
  uint constant Relay = uint(bytes32(Keys.Relay)) | UnboundedMin72Hint256;
90
89
  uint constant Context = uint(bytes32(Keys.Context)) | UnboundedMin48Hint512;
91
90
  uint constant Recover = uint(bytes32(Keys.Recover)) | UnboundedMin104Hint256;
package/codec/Writers.sol CHANGED
@@ -414,7 +414,7 @@ library Writers {
414
414
  /// @param cmd Command identifier to encode.
415
415
  /// @param value Native value to encode.
416
416
  /// @param input Command input to encode.
417
- function appendStep(Writer memory writer, uint cmd, uint128 value, bytes memory input) internal pure {
417
+ function appendStep(Writer memory writer, uint cmd, uint value, bytes memory input) internal pure {
418
418
  uint size = Sizes.Step + input.length;
419
419
  uint i = reserve(writer, size);
420
420
  Blocks.writeStep(writer.dst, i, cmd, value, input);
@@ -561,7 +561,7 @@ library Writers {
561
561
  }
562
562
 
563
563
  /// @notice Append a STEP block by copying its nested input from calldata.
564
- function copyStep(Writer memory writer, uint cmd, uint128 value, bytes calldata input) internal pure {
564
+ function copyStep(Writer memory writer, uint cmd, uint value, bytes calldata input) internal pure {
565
565
  uint size = Sizes.Step + input.length;
566
566
  uint i = reserve(writer, size);
567
567
  Blocks.copyStep(writer.dst, i, cmd, value, input);
@@ -58,7 +58,7 @@ abstract contract Bootstrap is CommandBase, DebitAccountHook {
58
58
  bytes32 account,
59
59
  bytes memory state,
60
60
  bytes calldata input,
61
- uint128 value
61
+ uint value
62
62
  ) internal returns (bytes memory output, uint credit) {
63
63
  if (state.length != 0) revert UnexpectedState();
64
64
  if (input.length % Sizes.Bootstrap != 0) revert Blocks.InvalidBlock();
@@ -68,7 +68,7 @@ abstract contract CashoutInternal is Cashout {
68
68
  bytes32 account,
69
69
  bytes memory state,
70
70
  bytes calldata input,
71
- uint128 value
71
+ uint value
72
72
  ) internal returns (bytes memory, uint) {
73
73
  if (value != 0) revert ValueNotAllowed();
74
74
  if (state.length != 0) revert UnexpectedState();
@@ -59,7 +59,7 @@ abstract contract CreditAccountInternal is CreditAccount {
59
59
  bytes32 account,
60
60
  bytes memory state,
61
61
  bytes calldata input,
62
- uint128 value
62
+ uint value
63
63
  ) internal returns (bytes memory, uint) {
64
64
  if (value != 0) revert ValueNotAllowed();
65
65
  if (input.length != 0) revert UnexpectedInput();
@@ -62,7 +62,7 @@ abstract contract DebitAccountInternal is DebitAccount {
62
62
  bytes32 account,
63
63
  bytes memory state,
64
64
  bytes calldata input,
65
- uint128 value
65
+ uint value
66
66
  ) internal returns (bytes memory, uint) {
67
67
  if (value != 0) revert ValueNotAllowed();
68
68
  if (state.length != 0) revert UnexpectedState();
@@ -9,7 +9,8 @@ using Executions for Execution;
9
9
  abstract contract RecoverPayableHook {
10
10
  /// @notice Override to recover a witness through `handler`.
11
11
  /// @param handler Port that should attempt recovery.
12
- /// @param resources Chain-specific resources assigned to the recovery attempt.
12
+ /// @param resources Opaque packed chain-specific resources, not plain native
13
+ /// value. EVM handlers extract the low 128-bit value lane with `useResourceValue`.
13
14
  /// @param key Recovery lookup key.
14
15
  /// @param witness Witness payload used to prove and replay recovery.
15
16
  /// @param funds Shared execution containing the source value budget.
@@ -13,8 +13,9 @@ abstract contract RelayPayableHook {
13
13
  /// the source state is consumed before destination success is known.
14
14
  /// @param portal Destination portal implementation's host ID. Implementations
15
15
  /// may validate or resolve it for their transport.
16
- /// @param resources Chain-specific destination resources. EVM adapters
17
- /// may interpret this as packed execution gas and destination value.
16
+ /// @param resources Opaque packed chain-specific destination resources, not
17
+ /// plain native value. EVM adapters extract the low 128-bit value lane with
18
+ /// `useResourceValue`; higher bits may encode execution gas or other data.
18
19
  /// @param account Destination command account.
19
20
  /// @param state Complete state forwarded into the destination context.
20
21
  /// @param input Input forwarded into the destination context.
@@ -162,7 +162,7 @@ abstract contract RepayInternal is Repay {
162
162
  bytes32 account,
163
163
  bytes memory state,
164
164
  bytes calldata input,
165
- uint128 value
165
+ uint value
166
166
  ) internal returns (bytes memory, uint) {
167
167
  if (value != 0) revert ValueNotAllowed();
168
168
  if (input.length != 0) revert UnexpectedInput();
@@ -109,7 +109,7 @@ abstract contract SettleInternal is Settle {
109
109
  bytes32 account,
110
110
  bytes memory state,
111
111
  bytes calldata input,
112
- uint128 value
112
+ uint value
113
113
  ) internal returns (bytes memory, uint) {
114
114
  if (value != 0) revert ValueNotAllowed();
115
115
  if (input.length != 0) revert UnexpectedInput();
@@ -8,7 +8,9 @@ using Executions for Execution;
8
8
 
9
9
  /// @title ExecutePayable
10
10
  /// @notice Admin command that forwards raw calldata to one or more target nodes.
11
- /// Each CALL block specifies a target node ID, packed resources, and raw calldata payload.
11
+ /// Each CALL block specifies a target node ID, opaque packed resources, and raw
12
+ /// calldata payload. Packed resources are converted to plain native value only
13
+ /// through `useResourceValue`.
12
14
  /// Only callable by the admin account.
13
15
  /// Unspent top-level `msg.value` is returned as native budget credit.
14
16
  abstract contract ExecutePayable is RawNodeCalls, AdminBase {
package/core/Access.sol CHANGED
@@ -106,9 +106,7 @@ abstract contract NodeAccess is AdminAccess, TrustAccess, NodeEvent {
106
106
  /// @param node Node ID to validate.
107
107
  /// @return The same `node` value if trusted.
108
108
  function ensureTrusted(uint node) internal view override returns (uint) {
109
- if (node == 0 || !nodes[node]) {
110
- revert AccessDenied();
111
- }
109
+ if (!nodes[node]) revert AccessDenied();
112
110
  return node;
113
111
  }
114
112
 
package/core/Calls.sol CHANGED
@@ -3,9 +3,6 @@ pragma solidity ^0.8.33;
3
3
 
4
4
  import {TrustAccess} from "./Access.sol";
5
5
  import {Nodes} from "../utils/Nodes.sol";
6
- import {Keys} from "../codec/Keys.sol";
7
- import {Sizes} from "../codec/Specs.sol";
8
- import {max32} from "../utils/Utils.sol";
9
6
 
10
7
  /// @dev Emitted when a trusted inter-node call fails.
11
8
  /// @param addr Contract address that was called.
@@ -13,6 +10,81 @@ import {max32} from "../utils/Utils.sol";
13
10
  /// @param err Revert data returned by the failed call.
14
11
  error FailedCall(address addr, bytes4 selector, bytes err);
15
12
 
13
+ /// @notice Execute a raw `command(bytes)` call and decode its state and credit results.
14
+ /// @dev The caller must validate and authorize the command before entering this helper.
15
+ /// Uses one scratch region for both call data and return data. Successful return data
16
+ /// must use the exact ABI layout of `(bytes, uint)`.
17
+ function rawCommandCall(
18
+ bytes4 selector,
19
+ address target,
20
+ uint value,
21
+ bytes32 account,
22
+ bytes memory state,
23
+ bytes calldata input
24
+ ) returns (bytes memory output, uint credit) {
25
+ assembly ("memory-safe") {
26
+ let scratch := mload(0x40)
27
+ let statelen := mload(state)
28
+ let ctxlen := add(56, add(statelen, input.length))
29
+
30
+ // ABI envelope for command(bytes).
31
+ mstore(scratch, selector)
32
+ mstore(add(scratch, 0x04), 0x20)
33
+ mstore(add(scratch, 0x24), ctxlen)
34
+
35
+ // CONTEXT(account, BYTES(state), BYTES(input)).
36
+ let ctx := add(scratch, 0x44)
37
+ // 0xc5769e23 = bytes4(keccak256("#context"))
38
+ mstore(ctx, or(shl(224, 0xc5769e23), shl(192, sub(ctxlen, 8))))
39
+ // `ctxlen` is dead after the header and can carry the complete call length.
40
+ ctxlen := add(68, and(add(ctxlen, 0x1f), not(0x1f)))
41
+ mstore(add(ctx, 0x08), account)
42
+ let stateblk := add(ctx, 0x28)
43
+ // 0x6911b332 = bytes4(keccak256("#bytes"))
44
+ mstore(stateblk, or(shl(224, 0x6911b332), shl(192, statelen)))
45
+ mcopy(add(stateblk, 0x08), add(state, 0x20), statelen)
46
+ let inputblk := add(add(stateblk, 0x08), statelen)
47
+ let inputlen := input.length
48
+ mstore(inputblk, or(shl(224, 0x6911b332), shl(192, inputlen)))
49
+ calldatacopy(add(inputblk, 0x08), input.offset, inputlen)
50
+
51
+ // ABI word alignment also covers the input header's full-word write. Advance
52
+ // the free memory pointer once, after the return-data size is also known.
53
+ let inputend := and(add(add(scratch, ctxlen), 0x1f), not(0x1f))
54
+ let success := call(gas(), target, value, scratch, ctxlen, 0, 0)
55
+ let retlen := returndatasize()
56
+ if iszero(success) {
57
+ // FailedCall(target, selector, returndata)
58
+ // 0x20577b07 = FailedCall(address,bytes4,bytes)
59
+ mstore(scratch, shl(224, 0x20577b07))
60
+ mstore(add(scratch, 0x04), target)
61
+ mstore(add(scratch, 0x24), selector)
62
+ mstore(add(scratch, 0x44), 0x60)
63
+ mstore(add(scratch, 0x64), retlen)
64
+ mstore(add(add(scratch, 0x84), retlen), 0)
65
+ returndatacopy(add(scratch, 0x84), 0, retlen)
66
+ revert(scratch, add(0x84, and(add(retlen, 0x1f), not(0x1f))))
67
+ }
68
+
69
+ if lt(retlen, 0x60) { revert(0, 0) }
70
+ returndatacopy(scratch, 0, retlen)
71
+
72
+ // Strictly validate the exact ABI layout for (bytes, uint). The returned
73
+ // byte array can then point directly into the copied returndata.
74
+ if iszero(eq(mload(scratch), 0x40)) { revert(0, 0) }
75
+ let len1 := mload(add(scratch, 0x40))
76
+ if gt(len1, sub(retlen, 0x60)) { revert(0, 0) }
77
+ let pad1 := and(add(len1, 0x1f), not(0x1f))
78
+ if iszero(eq(retlen, add(0x60, pad1))) { revert(0, 0) }
79
+ output := add(scratch, 0x40)
80
+ credit := mload(add(scratch, 0x20))
81
+
82
+ let retend := and(add(add(scratch, retlen), 0x1f), not(0x1f))
83
+ if gt(retend, inputend) { inputend := retend }
84
+ mstore(0x40, inputend)
85
+ }
86
+ }
87
+
16
88
  /// @title RawNodeCalls
17
89
  /// @notice Low-level inter-node call helpers without target authorization.
18
90
  abstract contract RawNodeCalls {
@@ -21,7 +93,7 @@ abstract contract RawNodeCalls {
21
93
  /// @param value Native value to forward in wei.
22
94
  /// @param data Encoded calldata to send.
23
95
  /// @return success True if the low-level call succeeded.
24
- function tryRawCall(uint node, uint128 value, bytes memory data) internal returns (bool success) {
96
+ function tryRawCall(uint node, uint value, bytes memory data) internal returns (bool success) {
25
97
  address addr = Nodes.addr(node);
26
98
  (success, ) = payable(addr).call{value: value}(data);
27
99
  }
@@ -31,7 +103,7 @@ abstract contract RawNodeCalls {
31
103
  /// @param value Native value to forward in wei.
32
104
  /// @param data Encoded calldata to send.
33
105
  /// @return out Return data from the successful call.
34
- function rawCall(uint node, uint128 value, bytes memory data) internal returns (bytes memory out) {
106
+ function rawCall(uint node, uint value, bytes memory data) internal returns (bytes memory out) {
35
107
  bool success;
36
108
  address addr = Nodes.addr(node);
37
109
  (success, out) = payable(addr).call{value: value}(data);
@@ -59,7 +131,7 @@ abstract contract NodeCalls is RawNodeCalls, TrustAccess {
59
131
  /// @param value Native value to forward in wei.
60
132
  /// @param data Encoded calldata to send.
61
133
  /// @return success True if the low-level call succeeded.
62
- function tryTrustedCall(uint node, uint128 value, bytes memory data) internal returns (bool success) {
134
+ function tryTrustedCall(uint node, uint value, bytes memory data) internal returns (bool success) {
63
135
  return tryRawCall(ensureTrusted(node), value, data);
64
136
  }
65
137
 
@@ -68,7 +140,7 @@ abstract contract NodeCalls is RawNodeCalls, TrustAccess {
68
140
  /// @param value Native value to forward in wei.
69
141
  /// @param data Encoded calldata to send.
70
142
  /// @return out Return data from the successful call.
71
- function trustedCall(uint node, uint128 value, bytes memory data) internal returns (bytes memory out) {
143
+ function trustedCall(uint node, uint value, bytes memory data) internal returns (bytes memory out) {
72
144
  return rawCall(ensureTrusted(node), value, data);
73
145
  }
74
146
 
@@ -81,74 +153,6 @@ abstract contract NodeCalls is RawNodeCalls, TrustAccess {
81
153
  }
82
154
  }
83
155
 
84
- /// @title CommandCalls
85
- /// @notice Trusted command-call helpers for contracts that route command nodes.
86
- abstract contract CommandCalls is NodeCalls {
87
- /// @dev Build `command(bytes)` calldata and its nested CONTEXT block in one allocation.
88
- /// Threaded state is copied from memory and step input directly from calldata.
89
- function encodeCommandCall(
90
- bytes4 selector,
91
- bytes32 account,
92
- bytes memory state,
93
- bytes calldata input
94
- ) internal pure returns (bytes memory data) {
95
- uint contextLen = max32(Sizes.B32 + 2 * Sizes.Header + state.length + input.length);
96
- uint paddedContextLen = (contextLen + 31) & ~uint(31);
97
- uint dataLen = 4 + 64 + paddedContextLen;
98
-
99
- // Reserve one scratch word because the final eight-byte block header is
100
- // written with mstore. Exclude that word from the returned calldata.
101
- data = new bytes(dataLen + 32);
102
-
103
- uint contextKey = uint32(Keys.Context);
104
- uint bytesKey = uint32(Keys.Bytes);
105
- assembly ("memory-safe") {
106
- mstore(data, dataLen)
107
- let out := add(data, 0x20)
108
-
109
- // ABI envelope for command(bytes).
110
- mstore(out, selector)
111
- mstore(add(out, 0x04), 0x20)
112
- mstore(add(out, 0x24), contextLen)
113
-
114
- // CONTEXT(account, BYTES(state), BYTES(input)).
115
- let context := add(out, 0x44)
116
- mstore(context, or(shl(224, contextKey), shl(192, sub(contextLen, 8))))
117
- mstore(add(context, 0x08), account)
118
-
119
- let stateBlock := add(context, 0x28)
120
- let stateLen := mload(state)
121
- mstore(stateBlock, or(shl(224, bytesKey), shl(192, stateLen)))
122
- mcopy(add(stateBlock, 0x08), add(state, 0x20), stateLen)
123
-
124
- let inputBlock := add(add(stateBlock, 0x08), stateLen)
125
- let inputLen := input.length
126
- mstore(inputBlock, or(shl(224, bytesKey), shl(192, inputLen)))
127
- calldatacopy(add(inputBlock, 0x08), input.offset, inputLen)
128
- }
129
- }
130
-
131
- /// @notice Encode and call a trusted command node.
132
- /// @param command Command node ID embedding the target selector.
133
- /// @param value Native value to forward in wei.
134
- /// @param account Command account identifier.
135
- /// @param state Current command state block stream.
136
- /// @param input Command input block stream.
137
- /// @return nextState Decoded command output state block stream.
138
- /// @return credit Trusted native value to add to the caller's execution budget.
139
- function callCommand(
140
- uint command,
141
- uint128 value,
142
- bytes32 account,
143
- bytes memory state,
144
- bytes calldata input
145
- ) internal returns (bytes memory nextState, uint credit) {
146
- bytes4 selector = Nodes.commandSelector(command);
147
- bytes memory data = encodeCommandCall(selector, account, state, input);
148
- return abi.decode(trustedCall(command, value, data), (bytes, uint));
149
- }
150
- }
151
-
152
156
  /// @title PortCalls
153
157
  /// @notice Trusted port-call helpers for contracts that route port nodes.
154
158
  abstract contract PortCalls is NodeCalls {
@@ -157,7 +161,7 @@ abstract contract PortCalls is NodeCalls {
157
161
  /// @param value Native value to forward in wei.
158
162
  /// @param input Port input block stream.
159
163
  /// @return success True if the low-level port call succeeded.
160
- function tryCallPort(uint port, uint128 value, bytes memory input) internal returns (bool success) {
164
+ function tryCallPort(uint port, uint value, bytes memory input) internal returns (bool success) {
161
165
  bytes4 selector = Nodes.portSelector(port);
162
166
  bytes memory data = abi.encodeWithSelector(selector, input);
163
167
  return tryTrustedCall(port, value, data);
@@ -168,7 +172,7 @@ abstract contract PortCalls is NodeCalls {
168
172
  /// @param value Native value to forward in wei.
169
173
  /// @param input Port input block stream.
170
174
  /// @return success True if the low-level port call succeeded.
171
- function tryCallPortCopy(uint port, uint128 value, bytes calldata input) internal returns (bool success) {
175
+ function tryCallPortCopy(uint port, uint value, bytes calldata input) internal returns (bool success) {
172
176
  bytes4 selector = Nodes.portSelector(port);
173
177
  bytes memory data = abi.encodeWithSelector(selector, input);
174
178
  return tryTrustedCall(port, value, data);
@@ -179,7 +183,7 @@ abstract contract PortCalls is NodeCalls {
179
183
  /// @param value Native value to forward in wei.
180
184
  /// @param input Port input block stream.
181
185
  /// @return Decoded port output block stream.
182
- function callPort(uint port, uint128 value, bytes memory input) internal returns (bytes memory) {
186
+ function callPort(uint port, uint value, bytes memory input) internal returns (bytes memory) {
183
187
  bytes4 selector = Nodes.portSelector(port);
184
188
  bytes memory data = abi.encodeWithSelector(selector, input);
185
189
  return abi.decode(trustedCall(port, value, data), (bytes));
@@ -190,7 +194,7 @@ abstract contract PortCalls is NodeCalls {
190
194
  /// @param value Native value to forward in wei.
191
195
  /// @param input Port input block stream.
192
196
  /// @return Decoded port output block stream.
193
- function callPortCopy(uint port, uint128 value, bytes calldata input) internal returns (bytes memory) {
197
+ function callPortCopy(uint port, uint value, bytes calldata input) internal returns (bytes memory) {
194
198
  bytes4 selector = Nodes.portSelector(port);
195
199
  bytes memory data = abi.encodeWithSelector(selector, input);
196
200
  return abi.decode(trustedCall(port, value, data), (bytes));
package/core/Pipeline.sol CHANGED
@@ -2,8 +2,11 @@
2
2
  pragma solidity ^0.8.33;
3
3
 
4
4
  import {Blocks} from "../codec/Blocks.sol";
5
+ import {TrustAccess} from "./Access.sol";
6
+ import {rawCommandCall} from "./Calls.sol";
5
7
  import {Cursors} from "../utils/Cursors.sol";
6
8
  import {InsufficientValue, OutOfBounds, UnexpectedState} from "../utils/Errors.sol";
9
+ import {unpackCommand} from "../utils/Nodes.sol";
7
10
 
8
11
  /// @notice Hook implemented by hosts that execute encoded step streams.
9
12
  abstract contract PipeHook {
@@ -16,27 +19,34 @@ abstract contract PipeHook {
16
19
  ) internal virtual returns (uint remaining);
17
20
  }
18
21
 
22
+ /// @notice Hook implemented by pipeline hosts that execute host-local commands.
23
+ abstract contract ExecuteHook {
24
+ /// @notice Execute one command whose node ID targets the current host.
25
+ /// @dev Implementations must revert for unsupported local command IDs.
26
+ function execute(
27
+ uint cmd,
28
+ bytes32 account,
29
+ bytes memory state,
30
+ bytes calldata input,
31
+ uint value
32
+ ) internal virtual returns (bytes memory output, uint credit);
33
+ }
34
+
19
35
  /// @title Pipeline
20
36
  /// @notice Core pipeline functionality shared by higher-level surfaces.
21
- abstract contract Pipeline is PipeHook {
22
- /// @notice Override to dispatch one piped step.
23
- /// Called once per STEP block. The returned state becomes the state passed to
24
- /// the next step, and the final returned state must be empty. Returned
25
- /// credit is added to the shared native-value budget before the next step runs.
26
- /// @param cmd Command node ID to invoke or handle.
27
- /// @param account Account identifier for the piped context.
28
- /// @param state Current threaded state block stream.
29
- /// @param input Step input block stream.
30
- /// @param value Native EVM value assigned to this step.
31
- /// @return output Updated state block stream for the next step.
32
- /// @return credit Trusted native value to add to the pipeline budget.
33
- function dispatch(
37
+ abstract contract Pipeline is TrustAccess, PipeHook, ExecuteHook {
38
+ function run(
34
39
  uint cmd,
35
40
  bytes32 account,
36
41
  bytes memory state,
37
42
  bytes calldata input,
38
- uint128 value
39
- ) internal virtual returns (bytes memory output, uint credit);
43
+ uint value
44
+ ) private returns (bytes memory output, uint credit) {
45
+ (bytes4 selector, address target) = unpackCommand(cmd);
46
+ if (target == address(this)) return execute(cmd, account, state, input, value);
47
+ ensureTrusted(cmd);
48
+ return rawCommandCall(selector, target, value, account, state, input);
49
+ }
40
50
 
41
51
  /// @notice Execute a STEP block stream through the pipeline.
42
52
  /// @dev Reverts with `UnexpectedState` if the final threaded state is non-empty.
@@ -55,18 +65,15 @@ abstract contract Pipeline is PipeHook {
55
65
  (uint abs, uint end) = Cursors.bounds(steps);
56
66
 
57
67
  while (abs < end) {
58
- uint cmd;
59
- uint128 value;
60
- bytes calldata input;
61
- (cmd, value, input, abs) = Blocks.unpackStep(abs);
62
- if (abs > end) revert OutOfBounds();
68
+ (uint cmd, uint value, bytes calldata input, uint next) = Blocks.unpackStep(abs);
69
+ if (next > end) revert OutOfBounds();
63
70
  if (value > budget) revert InsufficientValue();
64
71
  unchecked {
65
72
  budget -= value;
66
73
  }
67
- uint credit;
68
- (state, credit) = dispatch(cmd, account, state, input, value);
69
- budget += credit;
74
+ (state, value) = run(cmd, account, state, input, value);
75
+ budget += value;
76
+ abs = next;
70
77
  }
71
78
 
72
79
  if (state.length != 0) revert UnexpectedState();
package/core/Portal.sol CHANGED
@@ -20,7 +20,7 @@ abstract contract Portal is PortCalls, NodeAccess, UnresolvedEvent, ResolvedEven
20
20
  /// @param message Encoded port input to forward.
21
21
  /// @param value Native EVM value assigned to the forwarding attempt.
22
22
  /// @return miss Message digest recorded for recovery when forwarding fails; zero on success.
23
- function forward(uint port, bytes32 key, bytes calldata message, uint128 value) internal returns (bytes32 miss) {
23
+ function forward(uint port, bytes32 key, bytes calldata message, uint value) internal returns (bytes32 miss) {
24
24
  if (tryCallPortCopy(port, value, message)) return bytes32(0);
25
25
 
26
26
  miss = keccak256(message);
@@ -33,7 +33,7 @@ abstract contract Portal is PortCalls, NodeAccess, UnresolvedEvent, ResolvedEven
33
33
  /// @param key Recovery lookup key.
34
34
  /// @param witness Witness payload used to prove and replay recovery.
35
35
  /// @param value Native EVM value assigned to the resolution attempt.
36
- function resolve(uint port, bytes32 key, bytes calldata witness, uint128 value) internal virtual {
36
+ function resolve(uint port, bytes32 key, bytes calldata witness, uint value) internal virtual {
37
37
  if (unresolved[key] != keccak256(witness)) revert BadWitness();
38
38
 
39
39
  delete unresolved[key];
@@ -38,15 +38,19 @@ library Budgets {
38
38
  }
39
39
 
40
40
  /// @notice Deduct the EVM value lane of `resources` from `budget`.
41
- /// @dev EVM resources use the low 128 bits as native value/endowment.
41
+ /// @dev `resources` is not a native value. This helper explicitly extracts
42
+ /// its low 128-bit EVM value lane and widens that lane to a plain `uint`.
42
43
  /// @param budget Mutable budget to debit.
43
44
  /// @param resources Packed resources whose low 128 bits contain native value.
44
45
  /// @return value Native value to forward in wei.
45
- function useResourceValue(Budget memory budget, uint resources) internal pure returns (uint128) {
46
- return uint128(useValue(budget, uint128(resources)));
46
+ function useResourceValue(Budget memory budget, uint resources) internal pure returns (uint value) {
47
+ value = uint128(resources);
48
+ useValue(budget, value);
47
49
  }
48
50
 
49
51
  /// @notice Deduct the EVM value lane of `resources` from a scalar budget.
52
+ /// @dev `resources` is not a native value. This helper explicitly extracts
53
+ /// its low 128-bit EVM value lane and widens that lane to a plain `uint`.
50
54
  /// @param budget Remaining native value in wei.
51
55
  /// @param resources Packed resources whose low 128 bits contain native value.
52
56
  /// @return remaining Native value remaining after the deduction.
@@ -54,7 +58,7 @@ library Budgets {
54
58
  function useResourceValue(
55
59
  uint budget,
56
60
  uint resources
57
- ) internal pure returns (uint remaining, uint128 value) {
61
+ ) internal pure returns (uint remaining, uint value) {
58
62
  value = uint128(resources);
59
63
  remaining = useValue(budget, value);
60
64
  }
@@ -618,7 +618,7 @@ library Executions {
618
618
  /// @return cmd Decoded command identifier.
619
619
  /// @return value Decoded native value.
620
620
  /// @return input Decoded nested input.
621
- function unpackStep(Execution memory exec) internal pure returns (uint cmd, uint128 value, bytes calldata input) {
621
+ function unpackStep(Execution memory exec) internal pure returns (uint cmd, uint value, bytes calldata input) {
622
622
  uint cur = exec.decoders;
623
623
  uint end;
624
624
  (cmd, value, input, end) = Blocks.unpackStep(cur.absolute());
@@ -1029,7 +1029,7 @@ library Executions {
1029
1029
  /// @param cmd Command identifier to encode.
1030
1030
  /// @param value Native value to encode.
1031
1031
  /// @param input Command input to encode.
1032
- function outputStep(Execution memory exec, uint cmd, uint128 value, bytes memory input) internal pure {
1032
+ function outputStep(Execution memory exec, uint cmd, uint value, bytes memory input) internal pure {
1033
1033
  uint size = Sizes.Step + input.length;
1034
1034
  uint i = reserve(exec, size);
1035
1035
  Blocks.writeStep(exec.output, i, cmd, value, input);
@@ -1164,7 +1164,7 @@ library Executions {
1164
1164
  }
1165
1165
 
1166
1166
  /// @notice Append a STEP block to execution output by copying its nested input from calldata.
1167
- function outputCopyStep(Execution memory exec, uint cmd, uint128 value, bytes calldata input) internal pure {
1167
+ function outputCopyStep(Execution memory exec, uint cmd, uint value, bytes calldata input) internal pure {
1168
1168
  uint size = Sizes.Step + input.length;
1169
1169
  uint i = reserve(exec, size);
1170
1170
  Blocks.copyStep(exec.output, i, cmd, value, input);
@@ -1247,12 +1247,14 @@ library Executions {
1247
1247
  }
1248
1248
 
1249
1249
  /// @notice Deduct the EVM value lane of `resources` from the execution budget.
1250
- /// @dev EVM resources use the low 128 bits as native value/endowment.
1250
+ /// @dev `resources` is not a native value. This helper explicitly extracts
1251
+ /// its low 128-bit EVM value lane and widens that lane to a plain `uint`.
1251
1252
  /// @param exec Mutable execution whose budget is charged.
1252
1253
  /// @param resources Packed resources whose value lane should be spent.
1253
1254
  /// @return value Native value to forward in wei.
1254
- function useResourceValue(Execution memory exec, uint resources) internal pure returns (uint128) {
1255
- return uint128(useValue(exec, uint128(resources)));
1255
+ function useResourceValue(Execution memory exec, uint resources) internal pure returns (uint value) {
1256
+ value = uint128(resources);
1257
+ useValue(exec, value);
1256
1258
  }
1257
1259
 
1258
1260
  // -------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootzero/contracts",
3
- "version": "1.27.0",
3
+ "version": "1.28.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",
@@ -13,8 +13,9 @@ abstract contract DispatchPayableHook {
13
13
  /// @notice Override to dispatch an encoded payload to `portal`.
14
14
  /// @param portal Destination portal implementation's host ID. Implementations
15
15
  /// may validate or resolve it for their transport.
16
- /// @param resources Chain-specific destination resources. EVM adapters
17
- /// may interpret this as packed execution gas and destination value.
16
+ /// @param resources Opaque packed chain-specific destination resources, not
17
+ /// plain native value. EVM adapters extract the low 128-bit value lane with
18
+ /// `useResourceValue`; higher bits may encode execution gas or other data.
18
19
  /// @param payload Encoded payload ready for the transport layer.
19
20
  /// @param funds Execution used for source value available for transport fees
20
21
  /// and destination resource funding.
package/utils/Nodes.sol CHANGED
@@ -6,6 +6,21 @@ import {InvalidId} from "./Errors.sol";
6
6
  import {Ids} from "./Ids.sol";
7
7
  import {ensureAddr, isFamily, matchesBase, toLocalBase} from "./Utils.sol";
8
8
 
9
+ /// @notice Validate and unpack a command node into its ABI selector and target address.
10
+ /// @dev Validates only the command type prefix; chain locality and authorization are caller concerns.
11
+ /// @param cmd Command node ID to unpack.
12
+ /// @return selector ABI selector stored in bits [191:160].
13
+ /// @return target Contract address stored in bits [159:0].
14
+ function unpackCommand(uint cmd) pure returns (bytes4 selector, address target) {
15
+ uint32 command = (uint32(Layout.Evm) << 16) | (uint32(Layout.Node) << 8) | uint32(Layout.Command);
16
+ if (uint32(cmd >> 224) != command) revert InvalidId();
17
+
18
+ assembly ("memory-safe") {
19
+ selector := shl(224, and(shr(160, cmd), 0xffffffff))
20
+ target := and(cmd, 0xffffffffffffffffffffffffffffffffffffffff)
21
+ }
22
+ }
23
+
9
24
  /// @title Nodes
10
25
  /// @notice Encoding and decoding helpers for 256-bit node identifiers.
11
26
  ///