diff options
| author | Adam Soltys <[email protected]> | 2026-10-23 07:59:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-23 07:59:40 -0700 |
| commit | 6c3b49a559c90954bf5ae6dbfada73ec5b3409cc (patch) | |
| tree | f0b6acf7e2ddbf22be00002b9c0b86e23c3f64a3 /tools | |
| parent | b3cee21d7f6614c4ec0cf462ba4cfe3a3d4e308c (diff) | |
| download | coinosv3-6c3b49a559c90954bf5ae6dbfada73ec5b3409cc.tar.gz coinosv3-6c3b49a559c90954bf5ae6dbfada73ec5b3409cc.zip | |
Deep coins are renewed under every tier, free of the base fee, before the server's depth cap strands them
A coin's exit depth grows by one per off-chain hop; the server refuses to
cosign a spend of a coin at max_vtxo_exit_depth (100), so a wallet paying
hourly from one change chain has an unspendable coin within days. Survey of
the ASP's 2,279 unspent coins: median depth 7, 154 coins past 40 holding
7.7k sats, five already stuck at the cap.
The server now waives the refresh base fee when every input is at its
deep_waiver_depth (60; ArkInfo.fees.refresh carries it). The client mirrors
that in refreshFee, never selects a capped coin as an input (the shortfall
message says why), and flattens deep coins whatever the custody tier: the
open app in a round of their own, the worker with a standing order two
blocks out per coin. The default tier's copy names the one exception.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/ark/custody-test.js | 34 |
1 files changed, 34 insertions, 0 deletions
diff --git a/tools/ark/custody-test.js b/tools/ark/custody-test.js index f8106b7..214ab45 100644 --- a/tools/ark/custody-test.js +++ b/tools/ark/custody-test.js @@ -93,4 +93,38 @@ const coin = (id, amountSat, expiryHeight, extra = {}) => ({ id, amountSat, expi await assert.rejects(validateVtxo(v, { serverPubkey: serverPub, chain, acceptExpired: true }), (e) => e instanceof VtxoValidationError && /not confirmed/.test(e.message), 'past the expiry check, on to the anchor'); console.log('✓ validateVtxo: acceptExpired lets a coin past its timelock through the expiry check'); } + +// deep coins: the server's depth cap and its base-fee waiver, mirrored +{ + const m = mk(true); + m.info.refreshFees.deepWaiverDepth = 60; + m.info.maxVtxoExitDepth = 100; + m.exitDepth = (v) => v.depth || 0; + m._tipMemo = async () => 1000; + const shallow = coin('s', 5000, 2000, { depth: 7 }), deep = coin('d', 4000, 2000, { depth: 60 }); + const capped = coin('x', 3000, 2000, { depth: 100 }), near = coin('n', 2500, 2000, { depth: 92 }); + const expiredDeep = coin('e', 6000, 900, { depth: 70 }); + m.state.vtxos = [shallow, deep, capped, near, expiredDeep]; + assert.equal(m.refreshFee([deep], 1000), 0, 'a refresh of deep coins alone pays no base fee'); + assert.equal(m.refreshFee([deep, expiredDeep], 1000), 0); + assert.equal(m.refreshFee([deep, shallow], 1000), 300, '…one shallow coin in the set and it pays'); + assert.equal(m.refreshFee([shallow], 1000), 300); + assert.deepEqual(m.deepCoins().map((v) => v.id).sort(), ['d', 'e', 'n', 'x'], 'deep = at the waiver depth or within 8 hops of the cap, expired or not'); + assert.deepEqual(m._selectInputs(2900).map((v) => v.id), ['d'], 'a coin at the cap is never an input (the cosign would be refused)'); + m.state.vtxos = [capped]; + assert.throws(() => m._selectInputs(1000), /waiting to be renewed/, 'and the shortfall says why'); + m.state.vtxos = [shallow, deep, capped, near, expiredDeep]; + // the worker's orders: one per deep coin still inside its timelock, two blocks out + const orders = []; + m.scheduleRefresh = async (ids, h) => { orders.push([ids.join(), h]); (m.state.scheduled ||= []).push({ inputIds: ids, unlockHash: 'u' + ids }); }; + assert.equal(await m.ensureDeepRefreshes(3), 3); + assert.deepEqual(orders, [['x', 1002], ['n', 1002], ['d', 1002]], 'deepest first, at tip + 2; the expired one waits for the app'); + assert.equal(await m.ensureDeepRefreshes(3), 0, 'covered coins are not ordered twice'); + // no waiver and no cap advertised: nothing is deep + const plain = mk(true); plain.exitDepth = () => 999; plain.state.vtxos = [coin('p', 5000, 2000)]; + assert.deepEqual(plain.deepCoins(), []); + assert.equal(plain.refreshFee(plain.state.vtxos, 1000), 300, 'no waiver advertised: the base fee stands however deep'); + console.log('✓ deep coins: no base fee for a refresh of deep coins alone, a capped coin is never spent, the worker orders them flattened'); +} + console.log('\n✅ custody tiers behave'); |
