npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rootzero/contracts

v1.52.0

Published

Solidity contracts and protocol building blocks for rootzero hosts and commands.

Readme

rootzero

rootzero is a protocol for building hosts: contracts that expose a uniform set of endpoints over accounts and assets — commands that change state, queries that read it, and port links that connect hosts to each other, on the same chain or across chains.

This repository is @rootzero/contracts, the Solidity library for the EVM port of the protocol: the base contracts, block codecs, and helpers that rootzero applications compose.

Two decisions shape everything below. First, all data that crosses a host boundary is encoded in one binary block format, so an input means the same bytes on every chain. Second, every surface operates on runs of blocks rather than single values, so batching is the default, not a feature added later. This guide introduces the protocol bottom-up: blocks, then identities, then hosts and the endpoints built on top of them.

Quick Start

Scaffold a ready-to-run Hardhat project, or add the library to an existing one:

npx create-rootzero@latest my-app
# or
npm install @rootzero/contracts

A minimal commander-only host composes CommandHost with the endpoints it needs and implements their policy hooks:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.33;

import { Balances, CommandHost } from "@rootzero/contracts/Core.sol";
import { Deposit } from "@rootzero/contracts/Endpoints.sol";

contract ExampleHost is CommandHost, Balances, Deposit {
    constructor(uint commander) CommandHost(commander) {}

    function deposit(bytes32 account, bytes32 asset, uint amount) internal override returns (uint) {
        creditTo(account, asset, amount);
        return amount;
    }
}

CommandHost requires a nonzero local commander host ID and accepts command calls only from its embedded native caller. It has no built-in admin commands, peer registry, guardians, inbound introduction endpoint, generic execution command, or native-token receive function. Use Host instead when the application needs those advanced facilities; its commands accept the commander, the host itself, and explicitly authorized host callers.

Both host types introduce themselves during deployment when the native target encoded by the commander host ID is a contract. That commander must implement introduce(uint,uint) and accept the call, otherwise deployment reverts. Host IDs encoding EOAs do not receive an introduction call.

Host contracts are designed for fresh deployment rather than proxy upgrades. Releases may change inheritance storage layout, immutable configuration, and encoded identity formats; storage compatibility across versions is not supported.

Pipelines and trusted outbound port callers require CommandAccess and PortAccess implementations respectively. The advanced Host supplies both directly through its node policy, while CommandHost deliberately does not. A minimal host can implement narrow custom policies. Both access capabilities return the authorized endpoint's selector and address for direct use with the free raw-call helpers.

Deploy it with the local host ID encoding your native identity as commander and you can call its commands directly. An input is a run of binary blocks — here, a single #assetAmount block asking to deposit an asset (the encoders are a few lines each; see test/helpers/setup.ts and test/helpers/blocks.ts for reference implementations):

const commander = await hostId(deployer.address);
const host = await ethers.deployContract("ExampleHost", [commander]);

const account = encodeUserAccount(user.address); // receiving account
const input = encodeAssetAmountBlock(asset, 100n); // what to deposit
const context = encodeContextBlock(account, "0x", input);
await host.deposit(context); // logs the credited amount in the Deposit OUTPUT lane

The rest of this guide explains the ideas this example leans on — blocks, IDs, hosts, commands — and the surfaces built on top of them.

Blocks

Every input, response, and piece of in-flight state is a stream of typed blocks. A block is a four-byte key, a four-byte big-endian length, and a payload:

[bytes4 key][uint32 payloadLen][payload]

The key is usually bytes4(keccak256("#name")), and the payload layout is described by a schema body published under an alias. For example, the standard assetAmount block that requests a deposit:

assetAmount: { bytes32 asset, uint amount }

Use #assetAmount when the caller supplies both the asset and quantity. The scalar #amount { uint amount } block is available when the consumer already defines the asset or unit. It has a distinct 32-byte payload.

is 72 bytes on the wire: an 8-byte header followed by two big-endian 32-byte fields. There is no ABI encoding and no chain-specific type anywhere in the format — field types are chain-neutral integers, bytes, and booleans. A deposit input built for an EVM host is byte-for-byte the input a CosmWasm or Solana port would parse; what differs per chain is how a host resolves the identifiers inside, never how the bytes are laid out.

Schemas can express more than flat fields: a block may contain any number of nested child blocks (#bytes as payload names raw dynamic bytes), items can be marked many when they form a repeated list. Each declared child header is present, and its payload must satisfy that block's schema. Bytes, strings, and lists may have zero-length payloads; there is no universal empty-block marker. An at N hint can reposition one field in off-chain presentation without changing its wire position. Qualified schema names such as relay.input describe encoded block streams inside aliased #bytes fields, preserving ordinary block headers and decoder helpers; schemas emitted locally by the active host take precedence over trusted-context and standard schemas with the same name. Schema strings may start with an optional name: prefix. For example, assets: many #asset as assets names the schema assets and independently names its list item assets; helpers take the complete string without a separate name argument. Standard block aliases are intrinsic protocol metadata: indexers resolve names such as balance, step, and context from their standard keys even when no named schema annotation is emitted.

Entities such as commands and assets may carry a #counterparty { bytes32 account } annotation identifying their counterparty account, including host accounts. The latest trusted value replaces the earlier association, and account zero identifies Rootzero. The helper emits the claim without validating either ID or granting authority. The full schema language is specified in docs/Schema.md. The standard block schemas live in Schemas and their runtime keys in Keys (both via @rootzero/contracts/Codec.sol).

A rare top-level list is published as a custom schema consisting of one item, such as many #asset or { many #asset }. Its context-local schema key becomes the outer block key accepted by the endpoint; a list alongside sibling items continues to use the generic #list key.

Batches

An input is not a single struct; it is a run of blocks. One #assetAmount block asks for one deposit, five blocks ask for five, and the code path is identical — every endpoint parses with a cursor and loops until the stream is exhausted. The published lane key is the prime item: it is the block type that may repeat for batching. Each top-level block represents one operation; fixed compositions use a custom parent block.

Off-chain, building a batch is concatenation. Using the reference encoders from test/helpers/blocks.ts:

import { concat } from "ethers";
import { encodeAssetAmountBlock } from "./helpers/blocks";

const input = concat([
  encodeAssetAmountBlock(usdc, 250_000_000n),
  encodeAssetAmountBlock(dai, 250n * 10n ** 18n),
]);
// deposit(input) returns two #balance blocks and zero native budget credit

Everything downstream keeps this shape: commands loop over input blocks, posting ports loop over transactions, and pipelines loop over steps. Batching is never a special case.

IDs, Accounts, Assets, and Nodes

Everything the protocol touches - accounts, assets, chains, hosts, endpoints - is identified by one 32-byte word. The first byte selects the convention:

  • 0x00: null/unset ID.
  • 0x01: Rootzero-native structured ID.
  • 0x02: opaque ID, encoded as 0x02 || category || subtype || bytes29(hash). The hash preimage is not recoverable from the ID; use a lookup table or witness data when the native account, asset metadata, or node target is needed.
  • 0x03: EVM structured ID. The value can be deconstructed according to its EVM layout.

Opaque preimages start with:

[uint8 formatHash][uint8 category][uint8 subtype][payload...]

0x01 means keccak256. The category and subtype are copied into the ID and included in the hash, binding its visible type to the opaque identity. The remaining payload format is host/domain-specific until a future standard defines it.

Opaque and structured IDs share the same category and subtype taxonomy. This allows generic validation of an opaque account, asset, or node without exposing or resolving its native identity. The Solidity helpers below construct and deconstruct IDs.

Structured EVM IDs use:

[uint32 type][uint32 chainid][192-bit payload]

Embedded EVM addresses always occupy the low 160 bits:

Account / ERC-20: [type:4][chain:4][zero:4]    [address:20]
Node:            [type:4][chain:4][selector:4][address:20]

Account and ERC-20 encoders leave the middle four bytes zero. After validating the ID's representation and category, extract the address with address(uint160(uint256(id))).

where type packs [uint8 representation][uint8 category][uint8 subtype][uint8 flags]. A structured ID announces what it is (an account, an asset, a node) and which chain it lives on, and the payload usually embeds the underlying address. User accounts are chain-agnostic, while admin accounts are chain-local. Guardians are normal user accounts assigned a host-specific role. Assets are unique IDs in the same single-word form as accounts and nodes. Nodes are hosts, commands, ports, queries, and guards.

Asset subtype 0x00 identifies the default asset for representations that define one. Derived = 0x01 and Virtual = 0x02 remain reserved taxonomy values without dedicated helpers or a standardized payload. Erc20 = 0x03 identifies an ERC-20 asset.

The Rootzero asset is the singleton global ID [Rootzero][Asset][0][0] with zero chain and payload fields. Its exact value is 0x0103000000000000000000000000000000000000000000000000000000000000 on every chain. Access it as Assets.Rootzero; EVM chain-coin and ERC-20 asset IDs remain chain-local.

Opaque asset declarations use LOG0 with Codes.AssetAnnotate followed by an #assetPreimage { bytes32 asset, #bytes as preimage } block. Indexers read the asset ID from the block and retain the emitter as its publisher. The preimage uses [0x01][Asset][subtype][payload...], letting offchain indexers or witnesses verify and resolve [0x02][Asset][subtype][bytes29(keccak256(preimage))] assets.

The Utils.sol entry point provides the constructors and inspectors:

bytes32 account = Accounts.toUser(msg.sender); // chain-agnostic user account
bytes32 asset = Assets.toErc20(tokenAddress);  // ERC-20 asset ID
uint hostId = Nodes.toHost(address(this));       // host node ID
bytes32 hostAccount = Accounts.toHost(hostId); // deterministic host account
bytes32 opaque = Ids.toKeccak(preimage);  // preimage: 0x01 || category || subtype || payload

Host accounts use [Evm][Account][Host][0], a chain ID, and an address in bits [159:0]. Layout.Host is shared with host nodes; the category distinguishes them. Accounts.toHost(address) uses the current chain, while Accounts.toHost(uint) validates a host node prefix and preserves its chain and address, including remote chains. Accounts.isHost classifies the prefix; Accounts.host additionally requires a nonzero address. Neither grants authority or requires deployed code. A host account can be used with settle; the same host account can also identify a position handled by realize.

Account validation convention

Account identifiers remain bytes32 and are trusted internally. Validate accounts where untrusted user input enters the system, according to the caller's account policy, then pass them through internal execution without repeating account-format checks. Accounts constructed by canonical internal helpers from trusted inputs need no redundant validation. Encoding an untrusted address does not establish that it satisfies policy; validate such inputs at the boundary.

Commands accepting user-supplied account identifiers must validate them before using them, returning them in positions, or forwarding them to another host. A trusted command must still validate untrusted input: authorizing the command does not validate its inputs. Trusted peers are responsible for the validity of accounts they supply; receiving ports need not repeat account-format validation. A faulty trusted integration can supply malformed accounts, and the receiving host does not guarantee rejection under this convention.

Internal accounting hooks, including debitAccount, creditAccount, and book, may assume their account arguments satisfy the caller's account policy. Account format is separate from authorization, balance checks, and operation-specific requirements, such as a valid native payout address. Those requirements still apply. Zero amounts, zero counterparties, and absent sides retain their documented semantics; this convention adds no checks to skipped legs.

This is the intended integration convention, not a claim that all existing implementation checks have been removed or that every input boundary already enforces it. Validation helpers retain their documented checks; for example, Accounts.addr and sendChainAsset still check the EVM family and nonzero address, and sendChainAsset does so even for a zero amount. Existing host-specific checks also remain. No Account or ValidatedAccount wrapper type is required.

Hosts

A host is one contract assembled from mixins. The base Host brings access control and the admin surface (authorize, unauthorize, appoint, dismiss, annotate, executePayable) plus the guardian revoke action; you add the endpoints you need and the policy hooks they require. Keeping a ledger is optional: the Balances mixin provides an account-and-asset ledger; hosts store their own balances under Accounts.toHost(host), but a host can just as well implement commands that hold no persistent state in the host at all — forwarding funds elsewhere, or operating only on the state threaded through a pipeline.

Access capabilities used by commands, ports, pipelines, and guards are abstract hooks. The two host bases provide the concrete policies: CommandHost accepts commands only from its commander, while Host directly implements commander, admin, node, peer, port, command, and guardian access. Guardian state and enforcement are part of Host; there is no separate guardian policy contract. Hosts that implement the allowance hook can additionally inherit the opt-in RevokeAllowance guard, which accepts hostAsset { uint host, bytes32 asset } entries and always applies a zero allowance.

Trust is explicit and minimal. Each host receives an immutable commander host ID at construction. The EVM port validates that it is local, extracts its native address internally for caller checks, and derives the admin account from that native identity. Other contracts become callers only when their node ID is authorized into the host's trusted set, and guardians are accounts allowed to take protective actions. At deployment, a host introduces itself to its commander, which is how host topology becomes discoverable.

The ExampleHost in the quick start shows the resulting split, and it runs through the whole library: mixins implement the protocol mechanics (parsing, batching, discovery events), and small virtual hooks let the host decide policy — where funds come from, how the ledger is keyed, what gets emitted.

Commands

Commands are the write endpoints. Every command receives the same context:

struct CommandContext {
    bytes32 account; // acting account
    bytes state;     // block stream produced by the previous command
    bytes input;     // block stream for this invocation
}

Every command returns a state block stream and a trusted native credit. State is threaded into the next pipeline step, while credit replenishes the shared pipeline budget without validation against forwarded call value. Command trust is the authority boundary.

State is linear, not optional ambient context. A command is responsible for the entire state stream it receives: it must validate and consume it, transform and return it, forward it intact, or revert. A command must never succeed while silently ignoring or dropping supplied state. Published endpoint schemas remain discovery metadata; the command's decoding and loop implementation defines its runtime source semantics. A command that does not consume supplied state rejects it when closing, while takeState and takeStateFixed validate and consume a complete state stream before forwarding it. takeBalances validates every forwarded block as BALANCE and returns a cursor for relayBalancePayable to encode directly; empty state remains accepted. This is especially important for #position, because dropping it could silently discard an outstanding debt requirement.

The input carries instructions; the state carries live value. While a sequence of commands executes, #balance, #custody, and #position blocks in the state are the value being moved — produced by one command, consumed by the next. Balance carries { asset, amount }, custody carries { host, asset, amount }, and position carries the flat position { asset, amount, liability, debt, counterparty }. The counterparty field identifies the settlement counterparty. Zero identifies Rootzero and is handled by exact booking through Settlement.settle. Generic codecs preserve the field; settle passes all five fields as a Position memory struct to its hook for fulfillment under the host's authorization policy. Account format follows the boundary-validation convention. Settlement hooks take (account, position), plus Execution memory funds for funded settlement. Settlement accepts empty input and consumes any number of POSITION blocks. Producers enforce minimum asset amounts and maximum debts before emitting their final positions, using packed LIMITS where appropriate. Producers handle fees before supplying the position: amount is the final net receipt and debt is the final total payment. Settlement receives the final position, books zero-counterparty positions on the active account, and passes nonzero counterparties to the account hooks when exchanging the exact debt and asset quantities. It adds no fees and makes no separate host fee credits. Account hooks and custom BookHook implementations may trust account format while preserving applicable authorization and balance checks. Empty exchanges skip the account hooks and do not validate the counterparty.

Repay settles only the debt: POSITION → POSITION with empty input. Its RepayHook.repay(account, position) must satisfy the entire debt or revert, without mutating the position. The command then sets debt = 0 and preserves all other fields. Settlement.repay uses BookHook to debit the account and, for a nonzero counterparty, credit that counterparty with the exact payment. Zero debt skips booking. A following Settle consumes the remaining asset leg.

Every nonzero position counterparty is an account, including host accounts. settle can exchange balances with any account counterparty on the executing host. A host account can instead be realized by its host, whose hook compares against Accounts.toHost(host) and fulfills the obligation before returning zero. The subtype identifies the account, not the route: offchain metadata and available balances determine where to settle or realize. See counterparty semantics.

Hosts that maintain account balances implement settle; hosts without their own balance ledger implement realize. A production host chooses one model rather than exposing both for the same operation. Settlement supplies reusable ledger mechanics with a default settle implementation that applies final quantities exactly.

Either side of a position may be absent. An absent asset side is encoded as asset = 0, amount = 0; an absent liability side is encoded as liability = 0, debt = 0. This mirrors transaction blocks, where a zero from or to omits that side of the transfer. A one-sided position remains useful when a command must preserve position-shaped state for later composition; a liability-only position is the standard representation of debt. An asset-only value can also use the narrower #balance block.

#position is general live state rather than a persisted lending-specific debt record. Its liability side carries value owed or required; it pairs that liability with value acquired or controlled. A command may preserve or replace either side and return the resulting state for the next step; settle terminally consumes the position, exchanging with an account counterparty or applying a zero-counterparty booking. An optional checkPosition step validates limits before settlement. This supports swaps, borrowing, refinancing, collateral changes, callback obligations, cross-host claims, fees, netting, and other multi-step operations. Positions are transient representations and do not themselves create or erase an obligation recorded by an external system.

At settlement, the debt side is the final total payment and the asset side is the final net receipt, with producer fees already accounted for. Limits protect these quantities; they do not cover separate charges outside the position.

The quantity in a debt side is an exact obligation. A command that consumes debt = 100 must deliver, make available, or otherwise satisfy all 100, or revert. Transfer fees and other sourcing costs are paid in addition to the debt or reflected in the gross amount sourced upstream; they must not silently reduce the fulfilled quantity. When fulfillment produces custody, the amount that actually reaches custody must equal the debt. Partial fulfillment requires preserving the unsatisfied remainder in a position rather than consuming it.

SwapExactIn and SwapExactOut in commands/Swap.sol share the input schema bytes32 asset, uint amount, many #asset as hops and return one POSITION per SWAP input. For swapExactIn, asset and amount identify the input liability and exact debt; hops run forward toward the output asset. For swapExactOut, asset and amount identify the desired output; hops run in reverse toward the input asset. Both routes exclude the asset named in the fixed fields. For an A ? B ? C swap, exact-in encodes A with hops [B, C], while exact-out encodes C with hops [B, A]. Commands validate each ASSET block. Empty routes call no hook and produce a Position with equal asset/liability and amount/debt; the position is still returned and logged, and normal settlement rules apply. The exact-in hook takes (liability, debt, asset) and returns the amount received; the exact-out hook takes (asset, amount, liability) and returns the debt required. The command feeds each result into the next hop and builds one aggregate Position using the shared immutable Counterparty. A concrete host initializes that base with Counterparty(account) once, even when it inherits both swap commands. Hooks validate amounts and asset pairs and settle intermediate assets internally. Use position-constraint commands to check the resulting amounts separately.

The standard Deposit mixin shows the canonical shape: open the execution, decode its input, call the hook, and write the output run. Execution helpers route known state blocks (#balance, #custody, and #position) to state automatically; generic and custom-schema decoding consumes input:

function deposit(bytes calldata context) external onlyCommand returns (bytes memory, uint) {
    return runCommand(id, descriptor, context, depositOne);
}

function depositOne(Execution memory exec) private {
    (bytes32 asset, uint amount) = exec.unpackAssetAmount();
    amount = deposit(exec.account, asset, amount); // host policy hook
    exec.outputBalance(asset, amount);
}

A command announces itself when the host is deployed. Its constructor emits a discovery event carrying the packed state, input, and output lanes (spec plus codes), plus a human-readable label. Flags are carried by the endpoint ID; the helper separately returns an execution descriptor for efficient opening:

abstract contract MyCommand is CommandBase {
    uint private immutable id;
    uint private immutable descriptor;

    constructor() {
        (id, descriptor) = command("myCommand", Specs.Empty, Specs.AssetAmount, Specs.Balance, 0);
    }

    function myCommand(bytes calldata context) external onlyCommand returns (bytes memory, uint) {
        return runCommand(id, descriptor, context, myCommandOne);
    }

    function myCommandOne(Execution memory exec) private pure {
        (bytes32 asset, uint amount) = exec.unpackAssetAmount();
        exec.outputBalance(asset, amount);
    }
}

Callback runners in CommandBase, AdminBase, PortBase, GuardBase, and QueryBase provide the standard lifecycle: runCommand(id, descriptor, context, callback) processes command batches, runCommandOnce(id, descriptor, context, callback) invokes its callback exactly once, runAdmin(id, descriptor, context, callback) authorizes the admin context before processing a batch, including when its sources are empty, runPort(id, descriptor, input, callback) processes port batches and returns output plus remaining value credit, and runQuery(descriptor, input, callback) processes queries through an internal view callback and returns only response bytes. Each callback receives the shared Execution memory. Batch callbacks must consume an item on every invocation; runCommandOnce also invokes its callback for empty sources and rejects leftover data afterward. Entry-point access modifiers remain in place.

runCommand, runCommandOnce, and runAdmin use the registered id to prefix topic-free logs selected by nonzero lane codes (Lanes.create(spec, codes)). Before processing, exec.logContext(id, descriptor) emits selected STATE/INPUT containers together. Selected output is emitted as an OUTPUT container after processing; returned bytes remain the original stream. See command runner stream logs.

runPort logs selected INPUT before processing and OUTPUT afterward. runGuard(id, descriptor, input, callback) logs selected INPUT and processes a guard batch without output or a budget. Queries remain view-only and reject nonzero lane codes during registration.

For custom loops, call logContext or logInput immediately after opening. finish(id, descriptor) finalizes output and logs it when selected, without checking consumption or touching the budget. Use drainBudget() to return and clear credit. Use expectEnd() to require exact consumption, or close(id, descriptor) to combine that check, finalization, logging, and credit. The no-argument finish() and close() variants remain pure and silent. Do not append output after finalization.

Use <endpoint>One for per-item private callbacks, such as depositOne and portCreditAccountOne, and <endpoint>Once for whole-input callbacks passed to runCommandOnce, such as relayPayableOnce. Endpoint-specific names allow composing endpoint mixins: Solidity rejects conflicting private callback signatures in multiple base contracts. Keep explicit opening and closing for custom lifecycle work, such as pipe settlement after the entire batch.

Deposit hooks return the actual amount that becomes live #balance state. Implementations can therefore deduct external ingress fees or report an otherwise adjusted received amount without overstating the value passed to the next pipeline step. Account debits are exact internal bookkeeping operations: they must make the complete requested amount available or revert. Any account fee is charged in addition and does not reduce the resulting balance.

Commands can describe grouped lanes with GroupsAnnot, available through Core.sol and Commands.sol:

annotateGroups(id, "#state as (debit, credit), #output as (receipt, change)");

Only grouped lanes are listed. Their schemas come from the published endpoint specs; empty lanes remain empty. Counts and roles are off-chain hints, with no descriptor fields or runtime enforcement. An empty description clears previous hints.

Commands can publish execution estimates with ExecutionCost, available through Core.sol and Commands.sol:

executionCost(id, 10_000, 5_000); // base cost per invocation, additional cost per batch

This emits #executionCost { uint base, uint batch }. Estimated command cost is base + batch * batchCount, in destination-local execution units. A batch is one logical group processed by the command; grouped inputs count as one batch, not one per constituent block. The concrete host supplies estimates for its hooks. Pipeline and transport overhead, plus any safety margin, are added separately by the planner or destination adapter. These are advisory estimates, not enforced limits or guaranteed bounds. Unknown batch counts or missing annotations mean unknown cost. The latest trusted annotation replaces the previous estimate; zero values are valid estimates and do not clear metadata.

Execution opening now allocates its growable output buffer from the descriptor hint. openInput and openContext accept optional numerator/denominator arguments to scale that hint before allocation; Executions.scaleOutput remains removed. Output helpers contain no deferred initialization or scaling checks; buffers still grow when needed.

The final argument is a packed flags byte. Pass 0 for an ordinary endpoint, or compose values such as Flags.Funded, Flags.Admin, and Flags.AdminFunded from the command or endpoint package entry point. The same flags byte is copied into the endpoint ID, keeping runtime behavior and published endpoint metadata aligned. Flags.Handoff marks a command that takes ownership of the remaining pipeline, while Flags.HandoffFunded combines handoff behavior with native-value funding; bits 2 through 5 are unassigned, and bit 6 remains endpoint-defined. Lane codes select logging independently of this flags byte.

CashoutHook declares an abstract cashout(account, amount) hook. Hosts implement their payout policy and choose the accounting and events to emit. The hook has no ChainAsset inheritance or built-in flow logging.

The free sendChainAsset(account, amount) helper in core/Cash.sol, also exported by Core.sol and Endpoints.sol, transfers the exact amount to Accounts.addr(account), accepting EVM-backed account subtypes with a nonzero address. Failed transfers revert with the global SendFailed() error without copying recipient return data. Zero amounts still validate the account and call the recipient; hook implementations may skip zero amounts before calling. The helper performs no bookkeeping and emits no events. The host must authorize and fund the withdrawal, finalize accounting before calling it, and protect its entrypoints against reentrancy.

The standard commands cover the common ledger movements: bootstrap (source an initial balance and native-value budget), cashout (withdraw native #balance state), deposit and depositPayable (external funds in), settlePayable (funded settlement), withdraw and burn (funds out), debitAccount and creditAccount (internal movements), payout (deliver state to other accounts), realize (pass each position to realize(account, position); the hook fulfills it in the existing denominations and returns counterparty zero; input is empty, and an optional following checkPosition validates the result against POSITION_CONSTRAINTS), checkBalance (validate each balance's asset and amount against paired BALANCE_CONSTRAINTS and return it unchanged; ExecuteCheckBalance validates memory state directly), checkPosition (validate each position against paired POSITION_CONSTRAINTS and return it unchanged; ExecuteCheckPosition validates memory state directly), settle (consume asset-liability position state with empty input, including Rootzero-backed and liability-only positions), relayPayable (relay a pipeline without state), and relayBalancePayable (relay balance state and a pipeline to another portal).

Pipelines

A single command is rarely the whole story. A pipeline is a run of #step blocks executed in order within one transaction:

step { uint cmd, uint value, #input }

Each step names a command, the native value it may spend, and its input. The returned state threads into the next command and the final state must be empty. Each returned native credit replenishes the budget before running the next step, allowing one command to fund later commands. The standard bootstrap command consumes exactly one #bootstrap { uint budget, many #assetAmount as balances } block and returns one #balance per requested item, in order. Non-chain assets debit the account as requested. Assigned step value funds chainAsset balances first; their uncovered amounts and any shortfall against the minimum remaining budget debit chainAsset once after the loop. Excess assigned value remains available as returned credit. An empty balances list supports budget-only funding. Bootstrap logs actual nonzero debits in one Account/Bootstrap Balance stream after funding, with the combined chainAsset debit last. No debit means no log. Bootstrap is registered with command metadata but is only executable through local pipeline execution. This is the core of Pipeline.pipe:

uint cursor;
assembly ("memory-safe") {
    cursor := or(steps.offset, shl(32, add(steps.offset, steps.length)))
}
while (uint32(cursor) < uint32(cursor >> 32)) {
    uint cmd;
    uint value;
    (cmd, value, cursor) = takeStep(cursor);
    if (value > budget) revert InsufficientValue();
    unchecked { budget -= value; }
    (state, value, cursor) = run(cmd, account, state, value, cursor);
    budget += value;
}
if (state.length != 0) revert UnexpectedState();

Pipeline.pipe takes the available native-value budget as a uint and returns the remaining budget after every step has executed. The enclosing entrypoint settles that final value once.

portPipePayable shares one native-value budget across all supplied CONTEXT blocks. After all contexts execute, it calls cashin once for any nonzero remainder when the last context's account is nonzero, and returns zero native credit. Context ordering determines the recipient. Empty input or a zero final account skips cashin and returns the remainder as budget credit, without transferring native value back. The trusted peer is responsible for supplied account validity. An exhausted or zero budget skips the hook.

The EVM pipeline is deliberately coupled to the canonical wire layout for gas efficiency. It extracts command selectors, targets, and flags directly from command IDs and writes CONTEXT, STATE, INPUT, BYTES, and RELAY blocks directly in assembly. Any change to those ID fields or block encodings must update Pipeline at the same time; the general node and block helpers are not used on this hot path.

A handoff command retains the ordinary command subtype and carries Flags.Handoff in its endpoint ID. The reserved handoff envelope is:

relay { #input, #bytes as steps }

input is the handoff STEP's ordinary command input, while steps is the untouched remainder of the original pipeline. Pipeline.pipe constructs this envelope automatically for commands carrying Flags.Handoff, calls the command, and stops executing the transferred continuation locally. Pipeline authors therefore encode only the command's ordinary input in the handoff STEP. Relay implementations can publish a qualified relay.input schema to describe how offchain tooling should decode the ordinary keyed blocks inside that byte payload. Implementations can use Blocks.exact for exactly one block, or open the standard decoder and close it after consuming a batch. The standard relay commands pass the account and this input to their transport hook alongside a fully constructed canonical #context containing the account, forwarded state, and remaining steps, plus the handoff command's funds. The transport can therefore use the account directly and forward the context without reconstructing its protocol payload.

Handoff has four operational rules:

  • A host-local execute hook must return handled = false for handoff command IDs. The hook sees only ordinary STEP input; delegation lets Pipeline construct the RELAY envelope containing the continuation.
  • Handoff transfers STEP bytes, not the pipeline's complete native-value budget. The handoff command receives its assigned STEP value. Returned credit rejoins the source budget, and the enclosing source entrypoint settles the final remainder normally.
  • The handoff command must consume or forward the current state and return empty state. Because the continuation is no longer executed locally, returned non-empty state fails pipeline finalization with UnexpectedState.
  • A transport that completes asynchronously should relinquish only state that is safe before destination success is known. The standard relay commands are intended for empty or balance state; debt, position, and similarly fallible state must not be relayed this way.

A transfer, for instance, is a two-step pipeline: debitAccount turns an #assetAmount input into #balance state, and payout consumes that state toward a recipient. Because a pipeline is just blocks, it is also the unit of command batching. STEP, CALL, and RECOVER carry full-width native uint value drawn from the shared budget. CALL funds a local call; RECOVER funds its handler invocation. DISPATCH carries opaque, packed chain-specific resources for adapters that also need gas or runtime parameters. A resources word is never itself native value; EVM adapters use useResourceValue to extract its low 128-bit value lane before spending it.

Local execute adapters use Execute for fixed-stride decoding: validate stream size once, then decode at absolute positions. Memory unpackers have a Memory suffix; calldata input stays a cursor until bounds are extracted. The paired balance/position checks validate streams in place, and settlement copies each position into an independent struct. See Execute.

Hosts that implement a pipeline locally can inherit ExecuteBootstrap, ExecuteCashout, ExecuteDebitAccount, ExecuteCreditAccount, and ExecuteSettle to register canonical command metadata while executing their local command IDs through executeBootstrap, executeCashout, executeDebitAccount, executeCreditAccount, and executeSettle. The bootstrap and debit adapters decode fixed-stride AssetAmount calldata input directly; cashout, credit, and settle decode memory-backed pipeline state. All return handled = true and avoid an external self-call. The host's execute hook must authorize a command before returning true. Returning handled = false delegates a local command to its trusted normal external entrypoint. Pass the step value into each adapter. Bootstrap is pipeline-local rather than an externally callable command and may consume value for chain-asset balance; the other four return assigned value unused as pipeline credit. Their external command entrypoints remain nonpayable.

ExecuteAuthorize extends Authorize with an executeAuthorize adapter for NODE input and empty state, returning assigned value unused as pipeline credit. It checks enforceAdmin(account, address(this)) internally; the default Host policy therefore requires a self-managed host. Pipeline entrypoints must authenticate the account and prevent peer-supplied admin accounts. Dispatching authorizeId() directly to this adapter keeps local authorization available independently of the node allowlist. The external authorize endpoint retains its caller checks.

Positions also support backward-composed pipelines. In an exact-output route, the asset side can represent the desired result while the liability side represents the value currently required upstream. Each hop consumes one position, fulfills or transforms its current requirement, and returns the next position:

position(C, 100, C, 100)
→ position(C, 100, B, 50)
→ position(C, 100, A, 25)
→ settle()

This is backward composition, not backward execution: #step blocks still execute forward in their encoded order. Exact-output routing is only one use; other commands may transform the asset side, the liability side, or both.

Queries

Queries are the read endpoints: view functions that take a block-stream input and return a block-stream response, with the same batch shape as commands. The standard getBalance query takes a run of account-asset positions and answers each one in order. Query the host's own holdings by supplying its deterministic host account:

input:  accountAsset { bytes32 account, bytes32 asset }
response: accountBalance { bytes32 account, bytes32 asset, uint amount }

Like commands, every query announces its input and output specs at deployment; tooling resolves their keys through the published block schemas.

GetEntityCodes in queries/Entity.sol exposes entityCodes, which accepts #entity { uint entity } blocks and returns one #codes { uint codes } block per entity, preserving input order and duplicates. Empty input returns empty output. Entity identifiers use the same full-width representation as annotations; the hook defines supported kinds. Codes describe entity kinds and current conditions, not historical actions or effects. Zero codes means unknown or no condition reported; States.Inactive means explicitly inactive. Active/Inactive is optional for entities where it does not apply. The hook owns condition semantics and code packing; the query preserves its returned word. GetAssetCodes exposes the asset-specific assetCodes query. Its hook must return exactly one Active or Inactive state. Both query mixins and their matching GetAssetCodesHook and GetEntityCodesHook contracts are exported by Endpoints.sol.

Ports

Settlement and the book port share BookHook from core/Settlement.sol: book(Booking memory value). Settlement forwards it to its virtual scalar book(from, to, liability, debt, asset, amount) overload, which implements it by debiting debt from from before crediting amount to to, skipping zero amounts. Matching accounts or assets are not netted. Hosts may implement the hook directly while preserving those exact-leg and funding requirements. Settlement also calls it for both exact exchange transfers, liability first.

Calls.raw (from Core.sol) calls ports returning (bytes output, uint credit), with overloads for bytes memory data and validated calldata uint dataCur. They take (selector, target, value, data, expectEmpty), strictly decode the tuple, and preserve target failures in FailedCall. expectEmpty constrains the output bytes only. Callers authorize the target and must ensure returned credit is backed before adding it to their budget; the helpers do not transfer ETH back. All ports return this tuple. Nonpayable ports return zero credit; portDispatchPayable returns its unspent budget. portPipePayable settles its remainder through cashin and returns zero credit when the final account is nonzero; otherwise it returns the unspent budget. Calls.rawQuery decodes bytes-only query results. Calls.tryRaw has the same memory/cursor overloads and reports success without decoding returndata, with optional explicit gas limits. Cursor overloads trust validated bounds, ignore metadata, and do not advance the cursor. All Calls functions are internal helpers.

exec.rawCall(selector, target, value, data, expectEmpty) provides the same memory/cursor overloads with execution budget accounting. They debit the full-width native uint value before calling, add the returned trusted credit to exec.budget, and return only the output bytes. Recovery hooks pass their value directly. Dispatch adapters interpreting packed resources extract the EVM value lane before passing it to a local call. Target authorization and credit backing remain the caller's responsibility.

Ports are the host-to-host surfaces, callable only by trusted peer hosts. A trusted peer is a fully trusted extension of the receiving host. Admitting a peer authorizes it to use every port the host exposes. Peer trust is not a limited permission to perform selected operations.

Before admission, the host's maintainers must fully validate the peer's behavior against the host's invariants, including its externally reachable entrypoints, dependencies, and upgrade authority. The peer must prevent its callers from causing unauthorized or invalid actions on the receiving host. For example, a peer that credits backing must establish that backing before calling the credit port. The receiving host relies on that guarantee instead of proving it again for each operation. Changes to the peer or its dependencies require renewed validation of the trust decision.

The port authenticates the peer once at entry through onlyPeer. There is no additional peer-specific authorization by port, asset, direction, or individual operation inside the batch. This keeps repeated authorization out of the execution path. Parsing, balance sufficiency, arithmetic checks, and other operation invariants still apply; they establish validity, not different permissions for different trusted peers. A component that cannot be trusted with the full port surface must not be admitted as a trusted peer.

The central ports are batches all the way down:

  • portRequestAsset consumes assetAmount { bytes32 asset, uint amount } blocks and passes the authenticated peer, asset, and amount to a host hook. The hook validates asset support and applies the host's request and transfer policy.
  • portRequestAllowance consumes the same assetAmount blocks and lets the authenticated peer set its own asset allowance through the same authoritative hook used by the admin allowance command.
  • portBook consumes BOOKING blocks with fields from, to, liability, debt, asset, amount. Each 200-byte block describes one debit from from and one credit to to. Its input lane uses Specs.Booking without operation logging. Account hooks own updated-balance logging. Both accounts may be the same. It returns empty bytes and zero credit, and any malformed block or account-hook failure reverts the entire batch and any hook logs. The complete struct is decoded before book(value); zero quantities skip their legs. For transfers, use the same asset and amount on both sides. For a single-sided entry, explicitly set the omitted leg to zero debt or amount; a zero account alone does not omit a leg. The Tx struct and transaction helpers remain available, but this port consumes a single booking block per operation.
  • portCreditAccount and portDebitAccount consume accountAmount blocks and apply the operation to the specified account. To credit or debit a host, pass Accounts.toHost(host) as that account. The same account hooks handle both. Hook implementers emit Logs.accountBalance(account, asset, updatedBalance) after mutations; these ports do not also log the requested amounts.
  • portPipePayable consumes context blocks, each carrying an account, an initial state, and a run of steps — a complete pipeline delivered by another host, executed locally against the port call's shared value budget.

This is also the cross-portal mechanism. relayPayable and relayBalancePayable are handoff commands whose transport hooks decode their own destination and resource input and forward an already constructed command context, while portDispatchPayable dispatches an explicit portal payload. A Portal forwards ordinary incoming CONTEXT streams directly to its commander host's portPipePayable endpoint. The pipeline port settles any remainder to the last context's account when nonzero and returns zero credit. Empty input or a zero final account returns the unspent budget instead. The portal ignores all successful return data and does not decode the message or perform account settlement. Failed messages are retained by digest and may be replayed through a separately selected trusted recovery-handler port. A bridge adapter moves the raw bytes; the destination host parses them with the same cursor rules and runs the same pipeline loop. Nothing in the payload is EVM-specific — step commands are destination-local command IDs, and only the adapter boundary (native transfers, address resolution, signatures) is chain-specific. The parity rule for ports is strict: every chain's implementation must parse the same input bytes and produce the same output bytes for every endpoint.

Guards and Admin

Admin commands use the regular command shape but are gated to the host's admin account: trust management (authorize, unauthorize), guardian management (appoint, dismiss), metadata (annotate), optional asset gating (allowAsset, denyAsset, allowance), and raw calls (executePayable). AddPool and RemovePool in commands/admin/Pool.sol provide optional pool administration. addPool consumes consecutive pairs of ASSET_AMOUNT blocks and calls its hook with two AssetAmount values; removePool consumes ASSET pairs and passes their identifiers. Both publish #input as (first, second) grouping, accept empty batches, and return empty output. Incomplete pairs or hook failures revert the entire batch. Hosts define pair ordering, asset and amount validation, funding, and removal requirements. These commands identify pools by their asset pair; hosts with multiple pools per pair need a more specific identifier.

Guards go the other way: direct actions guardians can take without any command context — the default is revoke, which lets a guardian drop a trusted node immediately.

Events and Discovery

Hosts publish Endpoint and Annotation blocks for endpoint lanes, schemas and human-readable labels. Protocol logs use LOG0 with scope/action/state codes or endpoint IDs followed by block streams. Indexers preload the standard catalog and apply each endpoint's documented semantics to balances and other state. Code catalogs are exported by Utils.sol; Logs and the block codecs by Codec.sol. Legacy event mixins, EventEmitter, EventAbi and the Events.sol barrel are removed. Applications can still declare their own Solidity events.

Development

The default build uses Solidity 0.8.35 with viaIR: true, optimizer enabled at 200 runs, and the Cancun EVM target. Tests and benchmarks use those same settings. Evaluate gas optimizations under this configuration; older benchmark documents explicitly measured without viaIR are historical comparisons. Applications importing these internal libraries should use matching compiler settings when reproducing the measurements.

For repository development, npm test runs regular tests without the *.bench.test.ts suites. Run a focused test with npm test -- test/peer.test.ts, or filter the regular suite with npm test -- --grep "Port Entrypoints".

Run npm run bench for benchmarks, or npm run bench -- test/settlement.bench.test.ts for a specific benchmark. Use benchmarks for performance changes, baseline updates, and release checks. npm run test:all runs the complete suite. npm run typecheck checks TypeScript. Use npm test -- --list or npm run bench -- --list to inspect suite selection.

Using the Library

Import from the package entry points rather than deep paths:

  • @rootzero/contracts/Core.sol — Host, annotation helpers including CounterpartyAnnot, access control, Balances, Settlement, ExecuteHook, PipeHook, ForwardHook, Pipeline, Portal, validator
  • @rootzero/contracts/Commands.sol — CommandBase, Execution, Flags, annotation and codec helpers, and shared value types for authoring custom commands
  • @rootzero/contracts/Endpoints.sol — command, admin, port, guard, and query mixins, their hooks (including ExecuteHook and PipeHook), and Flags
  • @rootzero/contracts/Codec.sol — Blocks, Encoder, Execute, Cursors, Schemas, Execution/Executions, Flags, Keys, and Specs
  • @rootzero/contracts/Utils.sol — Ids, Nodes, Assets, Accounts, cursor, layout, and value helpers

Core.sol, Commands.sol, Endpoints.sol, and Utils.sol each export all shared protocol errors from utils/Errors.sol, including QueryFailed and SendFailed. Core-specific errors such as AccessDenied and FailedCall are exported by Core.sol.

Repo layout:

  • contracts/core — host, access control, balances, pipeline, validation
  • contracts/commands — standard commands and admin commands
  • contracts/ports — port surfaces for inter-host and cross-portal flows
  • contracts/guards — guardian direct actions
  • contracts/queries — read-only query endpoints
  • contracts/codec — block schemas, cursor parsing, buffers, and writers
  • contracts/utils — ids, nodes, assets, accounts, layout, ECDSA
  • docs — Schema.md (wire format and schema DSL)

Use this library to create a new rootzero host, implement a command, or reuse the protocol's block format in tooling. It is the shared protocol foundation, not an end-user application.

Optional output logs

Set output lane codes with Lanes.create(spec, codes) to have the endpoint runner emit one [endpoint ID][OUTPUT block] log after processing the batch. Output helpers only append blocks; returned streams remain unchanged. Both swap commands publish Actions.Swap in their output lane and use this format, including empty batches. Logs carry no implicit account. See runner logs for indexer rules. Application-defined ordinary events use their own supplied ABIs.