diff options
| author | Adam Soltys <[email protected]> | 2026-09-10 06:59:13 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-10 06:59:13 -0700 |
| commit | 435e9886620deff3d66286e3b85e97ed8f5fae53 (patch) | |
| tree | 03329d4d892385d55dc0585346c1533389b8ca84 /src | |
| parent | 57289fd3f87554e9981ddc2c63c3bba805d599bd (diff) | |
| download | coinosv3-435e9886620deff3d66286e3b85e97ed8f5fae53.tar.gz coinosv3-435e9886620deff3d66286e3b85e97ed8f5fae53.zip | |
Ark: local state stops growing forever — spent coins shed bytes, done actions shed hex.
pruneArkState runs on every manager save: a spent coin loses its signed
bytes once its spend is 30 days old and no in-flight action names it (a
spent coin can never take the exit those bytes exist for), and an action
done more than 7 days ago loses the output-vtxo hex it minted (the change /
HTLC coins live in `vtxos` in their own right). Ids, amounts, inputIds and
preimages stay for history rows and spend dedupe; failed ln-pays keep their
HTLC bytes for the refund. Selective and idempotent; unit-tested.
Measured on a live deep wallet the immediate saving is small (every recent
spend is inside the window) — what remains is the live coins' own chains,
which only a renewal shrinks. The change is a growth cap, not a diet.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
Diffstat (limited to 'src')
| -rw-r--r-- | src/ark/manager.js | 52 |
1 files changed, 52 insertions, 0 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js index 471c4e0..d40a65b 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -63,6 +63,57 @@ const EMPTY_STATE = () => ({ movements: [], // { id, type, amountSat, ts, status, detail } }); +// Local state must not grow forever. Two things did: a spent coin kept its +// signed bytes (useful only for an exit, which a spent coin can never take) +// and a done action kept every output vtxo it minted as hex (the change / +// HTLC coins live in `vtxos` in their own right). Both go once they are +// safely history — a spend older than SPENT_BYTES_AFTER with no in-flight +// action naming it, an action done longer than DONE_HEAVY_AFTER ago. Ids, +// amounts and inputIds stay: history rows and spend dedupe still need them. +// Mutates in place; idempotent; cheap (no decoding). +const SPENT_BYTES_AFTER = 30 * 86400_000; +const DONE_HEAVY_AFTER = 7 * 86400_000; +const ACTION_HEAVY = ['destBytes', 'changeBytes', 'destBytesList', 'changeBytesList', 'htlcBytesList', 'vtxoBytes', 'txHex', 'fundingTxHex', 'outputVtxos']; +const actionTs = (a) => Number(String(a.id || '').split('-').pop()) || 0; +export function pruneArkState(state, now = Date.now()) { + if (!state) return state; + const actions = state.actions || []; + const inFlight = new Set(); + for (const a of actions) { + if (a.step === 'done' || a.step === 'failed') continue; + for (const id of a.inputIds || []) inFlight.add(id); + for (const p of a.parts || []) if (p.inputId) inFlight.add(p.inputId); + if (a.inputId) inFlight.add(a.inputId); + } + // when was a coin last named by anything? (a spend leaves a movement or a + // done action naming it; an unnamed spent coin has no date and is kept) + const lastNamed = new Map(); + const name = (id, ts) => { if (id && ts) lastNamed.set(id, Math.max(lastNamed.get(id) || 0, ts)); }; + for (const m of state.movements || []) { + name(m.vtxoId, m.ts); + for (const id of m.inputIds || []) name(id, m.ts); + } + for (const a of actions) { + const ts = actionTs(a); + for (const id of a.inputIds || []) name(id, ts); + for (const p of a.parts || []) name(p.inputId, ts); + name(a.inputId, ts); + for (const id of a.htlcVtxoIds || []) name(id, ts); + } + for (const v of state.vtxos || []) { + if (v.state !== 'spent' || !v.bytes || inFlight.has(v.id)) continue; + const ts = lastNamed.get(v.id); + if (ts && now - ts > SPENT_BYTES_AFTER) delete v.bytes; + } + for (const a of actions) { + if (a.step !== 'done') continue; + const ts = actionTs(a); + if (!ts || now - ts < DONE_HEAVY_AFTER) continue; + for (const k of ACTION_HEAVY) if (k in a) delete a[k]; + } + return state; +} + export class ArkManager { constructor({ account, storage, arkUrl, esploraUrl, network = 'regtest', onUpdate, lnQuoteUrl, statusHint }) { this.account = account; // HDKey node; ark keys derived beneath it @@ -165,6 +216,7 @@ export class ArkManager { // derivations (history build, etc.) invalidate exactly when the data // changes instead of on a timer that re-cools between interactions this.state._rev = (this.state._rev || 0) + 1; + pruneArkState(this.state); this.storage.save(this.state); this.onUpdate(this); } |
