diff options
| author | Adam Soltys <[email protected]> | 2026-07-05 20:39:02 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-05 20:39:02 -0700 |
| commit | 15e0b236e7ff2cafcb439c891c790deea8ec16a7 (patch) | |
| tree | 960057559e60b4b2e624a7de9e4ac35b1ecb81ad /src/ark/refresh.js | |
| parent | 0408609fc68fa71ff705ef7a241880efefddcc3f (diff) | |
| download | coinosv3-15e0b236e7ff2cafcb439c891c790deea8ec16a7.tar.gz coinosv3-15e0b236e7ff2cafcb439c891c790deea8ec16a7.zip | |
Unilateral exit: put vtxos on-chain without the server
The trustless backstop behind the Ark card's promise. The vtxo's
genesis chain reconstructs to fully-signed txs (keyspend sigs for
cosigned/arkoor transitions, preimage script-path for hark leaves);
they are zero-fee v3 with P2A anchors, so each hop broadcasts as a
[parent, CPFP child] package via esplora POST /txs/package, one
confirmed hop at a time (TRUC allows a single unconfirmed parent).
After the vtxo tx confirms and the CSV exit delta passes, the claim
spends the delayed_sign leaf with only the user key into the wallet's
own receive address.
Settings gains 'Exit on-chain without the server' with live per-exit
status lines; the claim tx is labeled 'Ark exit' in History. Probing
uses /tx/:txid (definitive 404), never /tx/:txid/status, which answers
confirmed:false for unknown txids.
Also: a fresh visitor opening an ark gift link now adopts the gift's
network automatically instead of being sent to Settings.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src/ark/refresh.js')
| -rw-r--r-- | src/ark/refresh.js | 9 |
1 files changed, 6 insertions, 3 deletions
diff --git a/src/ark/refresh.js b/src/ark/refresh.js index e3be402..54a3fc8 100644 --- a/src/ark/refresh.js +++ b/src/ark/refresh.js @@ -68,8 +68,10 @@ function taprootFromMerkle(internalXOnly, merkleRoot) { const P = secp256k1.ProjectivePoint; const internalPoint = P.fromHex(concatBytes(Uint8Array.of(0x02), internalXOnly)); const outputPoint = internalPoint.add(P.BASE.multiply(BigInt('0x' + hex.encode(tapTweak)))); - const outputXOnly = outputPoint.toRawBytes(true).slice(1); - return { tapTweak, outputXOnly, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; + const outputCompressed = outputPoint.toRawBytes(true); + const outputXOnly = outputCompressed.slice(1); + const outputParity = outputCompressed[0] === 0x03 ? 1 : 0; + return { tapTweak, outputXOnly, outputParity, scriptPubKey: concatBytes(hex.decode('5120'), outputXOnly) }; } // Cosigned transition input: internal = musig(pubkeys), one timelock leaf @@ -86,7 +88,8 @@ export function harkLeafTaproot(userPub, serverPub, expiryHeight, unlockHash) { const expiryLeaf = tapLeafHash(timelockSignScript(expiryHeight, serverPub.slice(1))); const unlockScript = hashSignScript(unlockHash, internalXOnly); const unlockLeaf = tapLeafHash(unlockScript); - return { ...taprootFromMerkle(internalXOnly, tapBranch(expiryLeaf, unlockLeaf)), unlockScript, internalXOnly }; + // expiryLeaf doubles as the merkle path when spending through the unlock leaf + return { ...taprootFromMerkle(internalXOnly, tapBranch(expiryLeaf, unlockLeaf)), unlockScript, internalXOnly, expiryLeaf }; } // Forfeit claim output: internal = musig(user, server); |
