Skip to content

0.41.1 — a failed keychain read must not mint a new wallet - #115

Merged
VickyXAI merged 1 commit into
mainfrom
fix/keychain-read-failure
Aug 28, 2026
Merged

VickyXAI merged 1 commit into
mainfrom
fix/keychain-read-failure

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Two defects, both of which I introduced in the last two releases and found on a second review pass.

1. strict mode could mint a new wallet and orphan the funded one

keychainLoad() returned null for two different facts: the keychain has no entry, and the keychain could not be read.

Under BLOCKRUN_KEYCHAIN=strict the plaintext file is already gone. So a transient read failure — locked keychain, ACL denial, timeout — was read as "no wallet here" and fell through to getOrCreateWallet(), which does exactly what its name says. The user's funded wallet was still sitting in the keychain; the MCP just started signing with a fresh empty one.

keychain.ts names this scenario, at the read site, as the worst possible failure:

silently creating a SECOND wallet because we could not read the first one is the worst possible failure here — the user's funds appear to vanish

The comment identified the hazard. Nothing acted on it.

Reads now return a discriminated found / absent / error. Only absent may create a wallet; an error with no file left throws with the keychain fault and a pointer to BLOCKRUN_WALLET_KEY. A loud failure is recoverable; a silently substituted wallet is not.

auto mode was never exposed — the file it keeps is found first, before any keychain read. Only strict users could hit this, and only 0.41.0 shipped it.

2. A guard I wrote in 0.40.x asserted nothing

assert.ok(authMs - MUSIC_AUTH_MARGIN_MS < SUBMIT + BUDGET + TIMEOUT + authMs)

authMs appears on both sides, so this is true for every possible input. It was decoration presented as a structural guarantee — the same failure I flagged in #110's original test, reproduced by me.

The deadline calculation moved into pollDeadline(), and the test now exercises real code: the poll budget binds at today's constants, the authorization takes over above the crossover, and a poll attempted at the deadline is refused rather than issued.

Writing it honestly immediately paid: the crossover is 445s, not the 420s I had assumed, and the test failed until I corrected it. It then caught a second wrong claim — I had asserted a full-length poll starting at the deadline must finish before validBefore, which is false and irrelevant, since pollTimeoutFor returns 0 there. The real invariant is that the deadline stops a full margin short of validBefore.

Validation

  • npm test — 362 passed
  • npm run typecheck, npm run build — clean
  • Reverted the read-failure guard → keychain-precedence.test.ts fails
  • Reverted the clamp → poll.test.ts and the video clamp test fail

Each guard in this PR has been shown to fail when the code it protects is removed.

0.41.0's keychainLoad() returned null for two different facts: the keychain has
no entry, and the keychain could not be read. Under BLOCKRUN_KEYCHAIN=strict
the plaintext file is already gone, so a transient read failure (locked
keychain, ACL denial, timeout) was taken as "no wallet here" and fell through
to getOrCreateWallet(), which does exactly what its name says. The funded
wallet was still in the keychain; the MCP just started signing with a fresh
empty one.

keychain.ts names this scenario as the worst possible failure in a comment at
the read site. Nothing acted on it.

Reads now return a discriminated found / absent / error. Only absent may create
a wallet; an error with no file left throws with the keychain fault and a
pointer to BLOCKRUN_WALLET_KEY. auto mode was never exposed, since the file it
keeps is found first.

Also replaces a tautological guard I added in 0.40.x: it asserted
auth - margin < submit + budget + timeout + auth, which has auth on both sides
and holds for every input. The deadline calculation moved into pollDeadline()
and the test exercises it — budget binds today, the authorization takes over
above the 445s crossover, and a poll attempted at the deadline is refused.
Writing that test found the crossover is 445s, not the 420s I first assumed.
@VickyXAI
VickyXAI merged commit 0958560 into main Aug 28, 2026
1 check passed
@VickyXAI
VickyXAI deleted the fix/keychain-read-failure branch August 28, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants