Skip to main content
Both settlement contracts are derived, never accepted: an implementation compiles the leaves from its own data and compares the resulting address to the one it was quoted. That comparison is only as good as the byte layout behind it, so this page gives the exact bytes — the leaf grammar, every opcode, and a fully worked example whose addresses you can reproduce. Two contracts are covered. VHTLC settles the corridor routes (Corridor Swaps). The non-interactive swap contract settles the routes whose endpoints both use arkade (Asset Swaps). Any change to these bytes changes every address on both sides of a swap. A version mismatch surfaces as a refused quote at the address comparison, not as lost funds.

Taproot Envelope

Both contracts are taproot outputs with no key-path spend: the internal key is the BIP-341 NUMS point, so the only way to move the money is a leaf. The tree is assembled with btcd’s algorithm (txscript.AssembleTaprootScriptTree), not a Huffman builder: leaves are paired left to right, an odd trailing leaf merges into the last branch built, and the resulting branches are merged FIFO. For leaf counts that are not a power of two a Huffman tree produces a different merkle root, a different output key, and a spend the Arkade server rejects. Leaf order is therefore part of the address. The orders below are normative. An Arkade address wraps the output key together with the Arkade server key; the same tree also has an ordinary on-chain taproot address, and both are derived from the identical output key.

Leaf Grammar

Every leaf of both contracts is one of five closures. Signature checks are CHECKSIGVERIFY for each signer except the last, which is CHECKSIG. Numbers are pushed as minimal script-nums, little-endian: 1 through 16 use OP_1OP_16, everything else is a data push. A seconds-type relative timelock encodes as (1 << 22) | (seconds / 512), so 4096 seconds is 0x400008 and pushes as 03 08 00 40. An absolute locktime pushes the same way: 1800277200 is 04 d0 0c 4e 6b.

The preimage condition

All three preimage-gated VHTLC leaves (claim, unilateralClaim, nonInteractiveClaim) share one condition fragment, and the current construction length-checks the witness item before hashing it:
<h160> is ripemd160(sha256(P)), 20 bytes — OP_HASH160 of the witness item reproduces it from P itself. The wire carries sha256(P) as payment_hash; the second hash happens locally on both sides. Without the OP_SIZE 32 OP_EQUALVERIFY prefix the claim leaves accept any witness value whose HASH160 matches, whatever its length. These bytes are VHTLC.ScriptV2; the SDK also exports VHTLC.Script, which omits the prefix and so derives a different address from the same participant keys.

Covenant leaves and the Emulator tweak

A covenant leaf carries no covenant bytes. Its enforcement lives in an ArkadeScript — a separate program the Emulator, the covenant’s co-signing service, executes before it signs — and the leaf commits to that program through the Emulator’s key:
This is not taproot tweaking; it is plain point addition. The tweaked key then appears in the leaf as an ordinary x-only signer, which is why a covenant leaf is indistinguishable on-chain from any other multisig leaf — the constraint is enforced by the Emulator refusing to sign, not by Bitcoin. Recompute the tweak from the program bytes to verify a leaf commits to the covenant you expect. Arkade opcodes occupy the OP_SUCCESS range on-chain, so they are only ever valid inside an ArkadeScript, never in a tapscript leaf.

Worked Example

Every byte below is generated from these inputs.

VHTLC

Eight leaves on every corridor swap: VHTLC.ScriptV2 makes the two non-interactive leaves optional, but corridor quotes always request both, in both directions. sender funds the lockup and gets it back if the swap fails; receiver claims it with Pcontract positions, not roles, since which party fills each flips with direction (the assignment table is in Corridor Swaps). Leaf order, which is the order the tree is built from: The CSV tiers above are this example’s values. They come from the trader’s own Arkade server’s reported exit delay, rounded up to a multiple of 512 s — never from a quote. The derivation, and why unilateralRefund sits level with unilateralClaim, are in Corridor Swaps.
Witness: [serverSig, receiverSig, P, script, controlBlock].
No timelock and no condition: an immediate, cooperative hand-back.Witness: [serverSig, receiverSig, senderSig, script, controlBlock].
Witness: [serverSig, senderSig, script, controlBlock].
The condition fragment comes first, so its arguments sit on top of the initial stack — last in the witness array.Witness: [receiverSig, P, script, controlBlock].
Witness: [receiverSig, senderSig, script, controlBlock].
The only leaf whose timing can steal — a funder able to refund before a claimant holding P can claim takes that claimant’s money — so it opens last.Witness: [senderSig, script, controlBlock].
The receiver’s key does not appear at all. What pins the payout to the receiver is the covenant below, not a key — which is what lets this leaf be pushed while the receiver is offline.The tweaked key derives from the Emulator key and the covenant program: ArkScriptHash = d711889930d948d05d5091337c4f96081c01fec158bcf9e125ea3b5041471c16.Witness: [emulatorSig, serverSig, P, script, controlBlock].
No timelock, and no signature from the sender — the only refund path that survives a permanently lost sender key, though it still needs both of the signatures in the table above.Witness: [emulatorSig, receiverSig, serverSig, script, controlBlock].

The VHTLC covenants

Both covenant leaves commit to the same 43-byte program shape, differing only in the destination. It reads the output at the same index as the input being spent and requires that output to pay the pre-committed script at least the input’s value. nonInteractiveClaim, pinned to the receiver’s script:
nonInteractiveRefund is the same program with the sender’s program bytes at offset 5:
The destination is stored as the 32-byte taproot program, not the 34-byte scriptPubKey: the OP_1 OP_EQUALVERIFY pair already proves the version, so the prefix is dropped before it is committed. A spend with no output at the matching index simply cannot satisfy the leaf.

Tapleaf hashes and the tree

For the worked example: Eight leaves give a balanced tree of depth 3. The claim leaf’s merkle path is d6155f7e…29bf, 1c5b1597…4e9d, 46f0af6c…ac53, and the resulting output key is da2542078eae0e401d0b5a26a497960b335bf11f52d08ae4f3b3429ff8c5c838 — scriptPubKey 5120da2542078eae0e401d0b5a26a497960b335bf11f52d08ae4f3b3429ff8c5c838.

Non-Interactive Swap Contract

Two leaves, no timelock, no preimage. The deposit is either delivered against or handed back; nothing expires. fulfill is signed by the server and the tweaked Emulator key alone and is constrained by its covenant; cancel is a plain 2-of-2 of the user and the server, with no solver signature anywhere in the contract. There are two programs, chosen by what the fill must deliver. The deposit side is never inspected, which is why one contract carries BTC, an asset, or an asset-for-asset swap. makerWP is the funding side’s 32-byte taproot program and user its x-only key. These are contract positions, not roles — the names come from the program artifact. Leaf order is fulfill, then cancel.
Witness: [emulatorSig, serverSig, script, controlBlock].
Same shape as the want-BTC leaf; only the tweaked Emulator key differs, because the covenant it commits to differs.Witness: [emulatorSig, serverSig, script, controlBlock].
For asset id 0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0, group index 0, wantAmount 250000:
The txid is pushed reversed. Asset ids are displayed in RPC byte order and committed in internal order, so 0f1e2d3c…e1f0 becomes f0e1d2c3…2d1e0f in the covenant. Reversing at the wrong step derives a different address that nothing will ever fill.
No covenant and no timelock: the deposit goes wherever the spend says, and the user needs nobody but the Arkade server. A funded deposit is therefore always cryptographically spendable by the wallet that funded it, which is why the SDK marks the contract as escrow to keep it out of ordinary coin selection — nothing in the contract itself says “escrow”.
The two-leaf trees for the worked example: The cancel leaf is byte-identical across both programs — it binds only user and server — so both trees share the tapleaf hash bb96303c8b14e35fad880d1d0fc7169aa2187447f0b9bbcc26473113e48ab32d. Only fulfill differs, and only through the tweaked key its covenant produces.

Opcode Reference

Standard Bitcoin opcodes carry their usual values. The Arkade opcodes appearing above are extensions, valid only inside an ArkadeScript: The co-signing service is authoritative for their exact semantics; the stack effects above are what the programs on this page depend on.

Reproducing These Bytes

The lockup compiles from VHTLC.ScriptV2 in @arkade-os/sdk; the non-interactive swap from offerVtxoScript in @arkade-os/swap, whose two program artifacts — swap-want-btc.program.json and swap-want-asset.program.json, exported parsed as swapPrograms — are pure data, shared verbatim with any other implementation. The leaf bytes come off different accessors. VHTLC.ScriptV2 names each leaf (claimScript, refundScript, … nonInteractiveRefundScript), plus nonInteractiveClaimArkadeScript and nonInteractiveRefundArkadeScript for the two covenant programs; the swap contract’s leaves come off functionByName("fulfill" | "cancel"). Both inherit .scripts and .leaves from VtxoScript. Golden tests pin these exact values, so an implementation in another language can compare against them before it ever derives a live address; changing either contract requires coordinated deployment with the solver.

Sources