// Unilateral exit — put a vtxo on-chain WITHOUT the server's cooperation, // then claim it through the CSV exit leaf. This is the trustless backstop // behind "you can always exit on-chain". // // The vtxo's genesis chain reconstructs to fully-signed transactions // (signatures ride in the encoded vtxo). They are zero-fee v3 txs with P2A // anchors, so each hop needs a CPFP child spending its anchor plus one of the // wallet's on-chain coins — and TRUC (v3) topology allows only one // unconfirmed parent, so a deep chain exits one confirmed hop at a time. // [parent, child] pairs are submitted via esplora's POST /txs/package // (bitcoind submitpackage). After the vtxo tx confirms and exitDelta blocks // pass, the claim tx spends it via the delayed_sign leaf with only our key. import { hex } from '@scure/base'; import { sha256 } from '@noble/hashes/sha256'; import { ripemd160 } from '@noble/hashes/ripemd160'; import { schnorr, secp256k1 } from '@noble/curves/secp256k1'; import { concatBytes } from './proto.js'; import { pubkeyPolicyTaproot, txid } from './send.js'; import { vtxoTransactions, harkLeafTaproot, harkUnlockWitnessShape, tapLeafHash, taprootScriptSighash } from './refresh.js'; const u16le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff); const u32le = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff); const u64le = (n) => { const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, BigInt(n), true); return b; }; const varint = (n) => { if (n < 0xfd) return Uint8Array.of(n); if (n <= 0xffff) return concatBytes(Uint8Array.of(0xfd), u16le(n)); return concatBytes(Uint8Array.of(0xfe), u32le(n)); }; const sha256d = (b) => sha256(sha256(b)); // Segwit serialization: tx is the {version, locktime, inputs, outputs} shape // the ark modules use; witnesses is one item-array per input ([] = empty). export function serializeTxW(tx, witnesses) { const base = [ u32le(tx.version), varint(tx.inputs.length), ...tx.inputs.flatMap((i) => [i.prevout, Uint8Array.of(0x00), u32le(i.sequence)]), varint(tx.outputs.length), ...tx.outputs.flatMap((o) => [u64le(o.valueSat), varint(o.scriptPubKey.length), o.scriptPubKey]), ]; const wit = witnesses.flatMap((items) => [ varint(items.length), ...items.flatMap((w) => [varint(w.length), w]), ]); const full = concatBytes(u32le(tx.version), Uint8Array.of(0x00, 0x01), ...base.slice(1), ...wit, u32le(tx.locktime)); const stripped = concatBytes(...base, u32le(tx.locktime)); const weight = stripped.length * 3 + full.length; return { raw: full, vsize: Math.ceil(weight / 4) }; } // The vtxo's chain as broadcastable transactions, oldest first. // Throws if any transition lacks its signature (an unsigned chain can't exit). export const signedExitTxs = (vtxo, serverPub) => exitChain(vtxo, serverPub, false); // Just the vsize of each hop's transaction, with the same "can this chain // exit at all" checks — but no elliptic-curve work: the output keys are // stand-ins of the right length. For fee estimates only; nothing here is // broadcastable. export const exitTxVsizes = (vtxo, serverPub) => exitChain(vtxo, serverPub, true).map((t) => t.vsize); function exitChain(vtxo, serverPub, sizeOnly) { const items = vtxoTransactions(vtxo, serverPub, sizeOnly); return items.map((it, i) => { const t = vtxo.genesis[i].transition; let witness; if (t.type === 'cosigned' || t.type === 'arkoor') { if (!t.signature) throw new Error('vtxo chain is missing a signature: cannot exit'); witness = [hex.decode(t.signature)]; } else if (t.type === 'hashLockedCosigned') { if (!t.signature || !t.unlock?.preimage) { throw new Error('vtxo chain is missing its unlock preimage: cannot exit'); } const preimage = hex.decode(t.unlock.preimage); if (sizeOnly) { const shape = harkUnlockWitnessShape(sha256(preimage)); witness = [hex.decode(t.signature), preimage, shape.unlockScript, shape.controlBlock]; } else { const tp = harkLeafTaproot(hex.decode(t.userPubkey), serverPub, vtxo.expiryHeight, sha256(preimage)); const cb = concatBytes(Uint8Array.of(0xc0 | tp.outputParity), tp.internalXOnly, tp.expiryLeaf); witness = [hex.decode(t.signature), preimage, tp.unlockScript, cb]; } } else { throw new Error('unsupported genesis transition: ' + t.type); } const { raw, vsize } = serializeTxW(it.tx, [witness]); if (sizeOnly) return { vsize }; const idInternal = txid(it.tx); return { txid: hex.encode(idInternal.slice().reverse()), txidInternal: idInternal, hex: hex.encode(raw), vsize, anchorVout: it.tx.outputs
{"version":3,"file":"store.js","sourceRoot":"","sources":["store.ts"],"names":[],"mappings":";;AAAA,kBAAe;IACb,SAAS,EAAE,EAAE;IACb,SAAS,EAAE,EAAE;IACb,MAAM,EAAE,EAAE;IACV,SAAS,EAAE,EAAE;IACb,MAAM,EAAE,EAAE;IACV,KAAK,EAAE,EAAE;IACT,IAAI,EAAE,EAAE;IACR,QAAQ,EAAE,EAAE;IACZ,OAAO,EAAE,EAAE;IACX,WAAW,EAAE,EAAE;IACf,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE;IACrB,EAAE,EAAE,EAAE;IACN,MAAM,EAAE,EAAE;IACV,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;IACjB,IAAI,EAAE,EAAE;IACR,QAAQ,EAAE,EAAE;IACZ,QAAQ,EAAE,EAAE;IACZ,QAAQ,EAAE,EAAE;CACb,CAAC"}