SkyhookESMASTe: The Settlement Computer

Programs: Functions, Dispatch, and Value

Raw bytecode can calculate, but settlement programs also need the structure of ordinary software. ESMASTe’s program layer adds four things on top of the VM: named functions, dispatch by selector, hosting in a fresh contract for every execution, and native value calls. Together with lock-scripts they make up the complete settlement computer, in which committed inputs enter the machine, execution proceeds deterministically within measured bounds, and value leaves only through accounted calls, each of which can be audited independently.

Anatomy of a script

An ESMASL script has two parts: a header (.header) and a body (.text).

The header is verbose, but it has four main segments:

  1. Script public key (spk). A key generated specifically for this script (secp256k1 on Ethereum). Any opcode that requires authentication checks against it; setting it to 0x0 permanently disables all authentication for the script, and also voids any request to persist.
  2. Constant stack. A 4-byte count of elements followed by 32-byte elements, referenced in the body as c1, c2, c3 and so on.
  3. External stack definition. How the external stack is split into words. The external stack itself is supplied by the relayer or caller when the script is redeemed, and is referenced in the body as ext1, ext2 and so on.
  4. Miscellaneous. Implementation-dependent settings, such as the step budget and the optional LEFTOVER, PERSIST and SALT parameters described under hosting.

The body is a sequence of opcodes and their arguments, with magic numbers that refer to entries in the constant or external stack. The VM executes it as a push-and-pop stack machine. Two opcodes carry most of the value logic: CHECKSENDER, which takes a signature (v, r, s) and checks that the script’s key signed the calling sender, and TRANSFERNATIVE, which sends value to an address with a given gas allowance (the maximum allowance forwards 63/64 of the remaining gas). Because the runner contract is created holding the entire escrow, BALANCE always gives the amount still available, which is why one script can make several transfers.

Try a script

The demo below runs simplified ESMASL scripts in the browser. It starts with an atomic-swap release script in the style of a hash time-locked contract: it releases the escrow to the counterparty if both preimages match the committed hashes and the owner has signed the caller, refunds the owner once the deadline passes, and otherwise denies. Programs are built from blocks named after library aggregates such as stdlib.hashlock and stdfin.pay_all; expand any block to see the raw opcodes it compiles to. Change the inputs, drag blocks around, or start from scratch.

Build an ESMASL scriptInteractive demo
.header spk set · 5 constants · ext: 32-byte words · limit 64 steps
Program drag blocks to move them · ▸ shows the raw opcodes
Control
Payments
Finish
Caller's inputs
Value
Runner
Counterparty
Owner
Caller

Show the machine
      A simplified demonstration, not the Milestone 2 authoring layer, which produces real ESMASL programs. Blocks stand for library aggregates (the stdlib/stdfin names here are illustrative) and compile to the raw opcodes shown when you expand them. Preimages are hashed with real keccak-256; CHECKSENDER is simulated by the checkbox, and STATICCALL, which is still in development, is emulated by the payments slider. Value leaves the runner only through TRANSFERNATIVE; a TRAP, running past the step limit or ending without RETURN reverts everything; the same inputs always give the same outcome.

      Functions

      Programs register named functions when they are loaded. Each registration gives a name, a code range, and a skip rule that ensures the registration data itself is never executed as code, a classic implementation mistake. Calls resolve names to entry points through a registry held in the machine and keyed by hashed names.

      Misaligned, empty and out-of-bounds registrations all trap deterministically, so control can never jump to an undefined location. The registry lasts only as long as the execution, so every run starts from a clean slate.

      Dispatch and hosting

      Contracts never invoke programs directly, since that would extend trust to program authors that they haven’t earned. Instead, one SingletonRunner contract holds the interpreter, and each execution gets a contract of its own. The factory creates a fresh contract with CREATE2, and its constructor receives the entire escrow. The new contract is then called and delegatecalls into the SingletonRunner, which runs the program in that contract’s context and dispatches by four-byte selectors following Ethereum convention: numeric names are taken literally, and human-readable names are hashed to selectors exactly as standard tooling expects, so existing infrastructure works unchanged. When the run ends, the contract self-destructs, which Ethereum still permits in the transaction that created it, unless the script asked to persist.

      Because the address is new, the run starts from a clean slate: the contract’s whole balance is the escrow, which is why BALANCE always gives exactly the funds still available, and no earlier state, approval or blacklist can attach to it. Each contract executes once and is gone before the transaction ends, so no second execution can share its state.

      Leftovers

      A self-destructing contract sends whatever balance remains to a beneficiary. By default that is the deployment’s leftover address, a deployment parameter set and governed by whoever deploys the bridge, like the difficulty floor. A script can name a different destination with a LEFTOVER parameter in its header’s miscellaneous segment, or by executing the SETLEFTOVER opcode while it runs. The default exists so that funds left behind by a misaligned or faulty script can later be requested back, rather than being lost for good. The exact behavior is implementation-specific.

      Whether the default should instead be the swap’s requester, so that leftovers return to them without anyone having to ask, is a reasonable question for the committee to discuss. Ultimately it is the implementer’s choice, made per deployment.

      Persistent runners

      Two further header parameters change the host contract’s lifetime and address:

      • PERSIST skips the self-destruct, so the contract outlives the transaction along with any funds or assets it holds. A deployment can charge extra for this. The fee isn’t about lost gas refunds, which self-destruct no longer earns; it covers storage. An ordinary run keeps its working state, such as the script key and the function code it uses, in transient storage (EIP-1153), which costs about 100 gas per write and vanishes when the transaction ends. A persisting contract must instead write the script key and any function code it retains into permanent storage, at about 20,000 gas for each new slot, and that state stays on the chain indefinitely. The script’s creator keeps control of it through executeScriptOp, which takes a target address, a value, a gas allowance and calldata, together with a signature (v, r, s) that must verify against the script public key in the header, and performs that call from the contract. A script whose key is 0x0 can’t persist at all: the zero key tells the bridge that no sender authentication is required, so it treats any request to persist as void. A script that needs no authentication therefore never leaves behind a contract that nobody could control.
      • SALT sets the CREATE2 salt, which lets the contract’s address be predicted before it is created. A CREATE2 address also depends on the address of the bridge that creates it, so the prediction holds only if you know which bridge, on which chain, will execute the script. For a script written offline that is rarely certain, so a predicted address shouldn’t be relied on without some other guarantee, such as an agreement on which bridge will run it.

      Value calls

      The TRANSFERNATIVE instruction moves real funds. It pops three operands (an address, a value and a gas allowance) and makes a native value call, then pushes 1 if the call succeeded or 0 if it failed. The maximum allowance forwards 63/64 of the remaining gas. The program decides what to do next: it can retry, revert, or continue with a reduced scope. The usual way to make a payment mandatory is a JZ that jumps to a TRAP when the result is 0, which reverts the whole execution.

      One TRANSFERNATIVE, step by stepInteractive
      Operand stack (top first)
        What the runner does

        Steps charged for the call
        0
        TRANSFERNATIVE pops an address, a value and a gas allowance, in that order, and makes a native value call. The result is pushed for the program to act on: retry, revert, or carry on with less. The cost isn't estimated in advance; the runner measures the gas actually burned and charges it afterwards (about 36k per transfer). Reentrancy guards bracket the call. Values shown are illustrative.

        The machine doesn’t estimate the cost of a call in advance. It lets the call happen, measures the gas actually used, and charges exactly that afterwards, about 36,000 gas for a transfer. This ex-post accounting means that the reported cost of a call can never differ from the real one, whatever the program does. Reentrancy guards bracket every call, so any attempt to re-enter the runner finds the guard already engaged.

        See also