From 6c3b49a559c90954bf5ae6dbfada73ec5b3409cc Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 23 Oct 2026 07:59:40 -0700 Subject: 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 --- tools/ark/custody-test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'tools') 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'); -- cgit v1.2.3