Skip to content

fix(wallet-toolbox): pass trx to readSettings from verifyReadyForDatabaseAccess - #444

Merged
ty-everett merged 1 commit into
bsv-blockchain:mainfrom
imranterranode:fix/storageknex-readsettings-trx
Aug 9, 2026
Merged

fix(wallet-toolbox): pass trx to readSettings from verifyReadyForDatabaseAccess#444
ty-everett merged 1 commit into
bsv-blockchain:mainfrom
imranterranode:fix/storageknex-readsettings-trx

Conversation

@imranterranode

Copy link
Copy Markdown
Contributor

Follow-up to #439, which was closed as already-fixed — ef710c3 (#426) had already corrected getProvenOrRawTx on 2026-08-04. This is the sibling defect flagged in that review, branched from current main.

Summary

StorageKnex.readSettings took no trx parameter and called this.toDb() with no argument, so it always ran on the pool. verifyReadyForDatabaseAccess(trx) then dropped its own trx when lazily populating the settings cache:

async verifyReadyForDatabaseAccess (trx?: TrxToken): Promise<DBType> {
  this._settings ??= await this.readSettings()        // ← trx dropped

  if (this._settings.dbtype === 'SQLite') {
    await this.toDb(trx).raw('PRAGMA foreign_keys = ON;')   // ← trx used correctly

knex forces { min: 1, max: 1 } on the sqlite dialect, so there is exactly one connection. When the settings cache is cold and the first access happens inside a caller's transaction, that read asks for a second connection which can never be granted — the transaction will not release until the query returns, and the query cannot run until the transaction releases. After acquireConnectionTimeout it fails with:

KnexTimeoutError: Timeout acquiring a connection. The pool is probably full.
Are you missing a .transacting(trx) call?

verifyReadyForDatabaseAccess is on every write path, so any StorageKnex instance whose first database access is transactional self-deadlocks.

This restores an existing contract rather than changing an API

The abstract already declares the parameter, and every other implementation accepts it:

Declaration Signature
StorageReader.ts:77 abstract readSettings (trx?: sdk.TrxToken)
StorageIdb.ts:202 readSettings(_trx?: TrxToken)
StorageMySQLDojoReader.ts:89 readSettings(trx?: TrxToken)
getBeefForTxid.ts:340 readSettings (trx?: TrxToken)
StorageKnex.ts:137 readSettings () ← the only one dropping it

It is also the last remaining toDb() call in StorageKnex that cannot receive a caller's transaction.

Regression test

#426 corrected getProvenOrRawTx without locking the behaviour down, so this adds a test covering both call sites — verifyReadyForDatabaseAccess(trx) with a cold cache, and readSettings(trx) directly.

acquireConnectionTimeout is lowered to 5s so a regression fails in seconds rather than the 60s default. The setup deliberately migrates a storage and then discards it, because makeAvailable() primes _settings and would mask the bug entirely — the cache must be cold for the transactional read to be the first access.

Verified the test actually catches the defect. Reverting only StorageKnex.ts and keeping the test:

Tests: 2 failed, 2 total
KnexTimeoutError: Knex: Timeout acquiring a connection. The pool is probably full.
Are you missing a .transacting(trx) call?

With the fix applied: Tests: 2 passed, 2 total.

Testing

Build green across the dependency chain. Full package suite on this branch (Node 24.13.0, pnpm 10.33.2):

Test Suites: 5 failed, 1 skipped, 191 passed, 196 of 197 total
Tests:       1 failed, 6 skipped, 1708 passed, 1715 total

The 5 failures are pre-existing and unrelated — verified by running the same suites on clean main and getting identical results. Four are Cannot find module 'vitest' (vitest suites picked up by jest in client/test/, mobile/test/), and the fifth is benchmarks/action-batch.bench.test.ts. Neither touches storage.

Why this matters downstream

Same failure mode as #426: the proof-fetching task dies before processing any request, proven_tx_reqs backlogs at zero attempts, and BEEF payment ancestry never terminates at a proven parent — so payment headers grow unbounded until Cloudflare's 32 KB request-header cap refuses them. That growth is a sawtooth when proofs arrive and a one-way ratchet when they do not.

Measured on the published npm 2.4.4 (which predates #426) in a locally-built BSV Desktop against a sandbox wallet making real mainnet payments: patching the equivalent line moved proven_tx_reqs completed 18 → 119, outstanding 139 → 38, and the payment header 33,058 → 13,666 bytes — a wallet Cloudflare had been refusing started paying again. This PR does not change that ceiling; it removes one more way to deadlock into it permanently.

…baseAccess

StorageKnex.readSettings took no trx parameter and called this.toDb() with no
argument, so it always ran on the pool. verifyReadyForDatabaseAccess(trx) then
dropped its own trx when lazily populating the settings cache:

  this._settings ??= await this.readSettings()

knex forces {min:1,max:1} on the sqlite dialect, so when that cache is cold and
the first access happens inside a caller's transaction, the settings read asks
for a second connection that can never be granted: the transaction will not
release until the query returns, and the query cannot run until the transaction
releases. It fails with "KnexTimeoutError: Timeout acquiring a connection. The
pool is probably full. Are you missing a .transacting(trx) call?".

verifyReadyForDatabaseAccess is on every write path, so any StorageKnex whose
first database access is transactional self-deadlocks. It was already using trx
correctly for the PRAGMA two lines below.

This is the same defect as ef710c3 (bsv-blockchain#426) fixed in getProvenOrRawTx, and the
last remaining toDb() call in the file that cannot receive a caller's
transaction.

The abstract already declared the parameter -- StorageReader.readSettings(trx?:
sdk.TrxToken) -- and StorageIdb, StorageMySQLDojoReader and getBeefForTxid all
accept it. Only StorageKnex dropped it, so this restores the existing contract
rather than changing an API.

Adds a regression test with acquireConnectionTimeout lowered to 5s so a
regression fails in seconds rather than the 60s default. Verified it fails
without the fix (both cases, with the KnexTimeoutError above) and passes with
it. bsv-blockchain#426 corrected getProvenOrRawTx without locking the behaviour down; this
covers both call sites.
@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@ty-everett
ty-everett merged commit 4958767 into bsv-blockchain:main Aug 9, 2026
3 checks passed
@imranterranode
imranterranode deleted the fix/storageknex-readsettings-trx branch August 10, 2026 08:27
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