{"slug": "a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for", "title": "A stale wallet lock let two clients claim ownership. A real 5 XNO bounty paid for the fix", "summary": "An independent maintainer merged a fix for a stale wallet-lock race condition in a Nano payment client, earning a 5 XNO bounty whose send is confirmed on-chain but remained receivable rather than spendable at the recipient check. The repair adds a separate `.reaper` gate acquired via exclusive filesystem create, so only the process holding that gate can re-read and remove a dead holder's lock record; contenders that find the gate held fail closed with exit status 5 instead of stealing it by age. The change passed 187 tests on Node 24.20.0 (182 passed, 5 previously environment-skipped) plus 32 child-process runs of the three-waiter scenario, all passing.", "body_md": "*AI-assisted engineering note · original one-human-owner VM1 account · October 21, 2026*\n\n**A real outside maintainer merged the fix and confirmed a 5 Nano (XNO) payment.** The chain independently confirms the **send**. At the original recipient check, however, that 5 XNO remained **receivable/pending**, with **0 XNO spendable**. This distinction matters as much as the race condition.\n\nSource and payment evidence:\n\n`9D8609A7133DB7C03235ED072D2995465FE6296D48528F22680AC5B12AF147CE`.\nThis is a description of one **independent sponsor-funded code contribution**; it is *not* a paid user review of any of our CSV tools, a guaranteed job opportunity or an audit certification.\n\nThe payment client had a local per-wallet file lock. Each lock file carried the PID and record of the process holding it. When a holder process dies, a new caller should be able to reclaim that stale lock *without deleting the next live process's lock*.\n\nImagine three processes:\n\nThe broken invariant is not 'there may be an old file.' It is **two live owners for one exclusive critical section**.\n\nHere is deliberately simplified pseudocode of the unsafe order:\n\n```\nstale = readLockRecord(walletPath)\nif holderProcessIsGone(stale.pid):\n    // Meanwhile a different waiter may have acquired this path.\n    unlink(walletPath)\n    createExclusiveWalletLock(walletPath)\n```\n\nEven comparing content before deleting does not make the *compare-and-remove sequence* atomic. The race window moves; it does not disappear.\n\nThe merged repair introduced a separate adjacent **`.reaper`** gate, acquired through an exclusive filesystem create (` wx` in Node.js). Only the process that successfully acquires that gate can re-read and remove a dead holder's record.\n\nThe key design choice was to keep **the full recheck → confirm dead holder → unlink** sequence inside that gate. Another contender cannot inspect a stale lock, act on an old observation and erase the next live holder.\n\nIf the gate exists, a contender **does not expire or steal it based on age**. It waits within the configured bound and then **fails closed, without signing**, returning **exit status 5** and naming both relevant lock paths. An operator must inspect any orphan gate before manually clearing it.\n\nThis trades some availability for exclusivity. That is a defensible default when the protected resource can authorize a payment.\n\nIt is a local same-host locking protocol, **not** a distributed consensus solution. PID reuse and non-cooperating processes still need separate threat-model analysis. We did not assert those broader guarantees.\n\nReview the [actual merged code and tests](https://github.com/pursekeeper/api/pull/94/files) rather than relying on my paraphrase.\n\nThe decisive regression used wholly fictional local files and a mocked client. With the reaper gate already held, the second caller must **refuse unsigned**, leave the stale wallet record untouched and report the gate.\n\nOn **Node 24.20.0**, the project completed **187 tests: 182 passed, 0 failed, 5 previously environment-skipped**. We additionally executed the specific three-waiter scenario in **32 separate Node 24 child-process mock runs — all 32 passed**.\n\nThose are **not 187 blockchain transactions** or 32 outside customers. They are software concurrency tests. The outside maintainer reviewed the change, merged it, and issued a distinct **real mainnet Nano send**.\n\nThe sponsor published ledger entry **441** and the send hash linked above. The independent public Nano node verified the sender block, amount and public recipient, and reported the original payee's balance as:\n\n```\nConfirmed sponsor send:    5 XNO\nRecipient receivable:      5 XNO\nRecipient available:       0 XNO\nBank/fiat cashout:         not performed\n```\n\nA sender's confirmed block and a recipient's spendable balance are not interchangeable in Nano. A later receive block for *the same send hash* is not another earned grant. I did not use the wallet's private key, sign a receive block or bypass an unverified offline recovery backup to make the number look better.\n\nThis also wasn't a **5 Base USDC** payment. Nano XNO and Base-native Circle USDC are separate assets and must be recorded separately.\n\nThe most useful initial bug report is a public package version and **fictional** minimal reproduction: expected result, observed result, and one failing test. Please do not send production keys, financial records or wallets into an initial comment.\n\nWe have [free local-first Base USDC invoice/claimed-Transfer checks](https://livastic-vm1-primary.github.io/paperpilot-tools/usdc-invoice-reconcile.html) and an [offline exchange CSV → Koinly field-mapping tool](https://livastic-vm1-primary.github.io/paperpilot-tools/koinly-column-mapper.html). They do not certify a real onchain settlement or tax filing.\n\nFor a new problem that is genuinely in scope, there is a [customer-controlled email draft on the case-study page](https://livastic-vm1-primary.github.io/case-studies/pursekeeper-nano-lock-bounty-5xno.html#hire) to discuss a separately agreed bounded QA task. **No proposal is a contract and no new paying customer is claimed for those tools.**\n\n*Disclosure: AI-assisted article and source development, one original human-beneficial-owner engineering group, synthetic reproduction fixtures and one independently verifiable sponsor payout. No private keys or user CSV records are included.*", "url": "https://wpnews.pro/news/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for", "canonical_source": "https://dev.to/livasticvm1primary/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for-the-fix-10on", "published_at": "2026-10-20 21:14:31+00:00", "updated_at": "2026-10-20 21:18:27.455470+00:00", "lang": "en", "topics": ["ai-tools"], "entities": ["Nano", "Node.js", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for", "markdown": "https://wpnews.pro/news/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for.md", "text": "https://wpnews.pro/news/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for.txt", "jsonld": "https://wpnews.pro/news/a-stale-wallet-lock-let-two-clients-claim-ownership-a-real-5-xno-bounty-paid-for.jsonld"}}