Skip to content

fix(wallet): make managed change starvation-resistant - #457

Merged
BraydenLangley merged 8 commits into
mainfrom
codex/wallet-utxo-liquidity-policy
Aug 11, 2026
Merged

fix(wallet): make managed change starvation-resistant#457
BraydenLangley merged 8 commits into
mainfrom
codex/wallet-utxo-liquidity-policy

Conversation

@ty-everett

@ty-everett ty-everett commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Program and scope

  • Tracker or issue: follow-up to fix(wallet-toolbox): isolate and resume action batches #448; addresses the managed-change starvation, excessive-input, BEEF-growth, and delayed-permission failure modes discussed around feat(sdk): let a 402 declare txids the payer may omit from payment ancestry #445, fix(wallet): settle permission grants before resuming #450, and fix(wallet): prevent delayed-broadcast funding starvation #452.
  • Program gate(s) advanced: wallet-toolbox 2.7.0 lockstep release (combined with the TTN candidate now on main); official wallet-infra configuration; progressive migration of pre-fee-change wallets.
  • Why this change is needed: the historical managed-change policy was optimized for an era when a 32-satoshi unit was still useful. At current fee assumptions, real wallets can accumulate many tiny outputs and then consume very large input sets for ordinary or permission-token actions. That reduces independent liquidity, enlarges BEEF payloads, increases signing and validation work, extends fragile unconfirmed ancestry, and can starve concurrent actions. The proposed latency-increasing permission workarounds address symptoms while leaving that pool pathology intact.
  • Explicitly out of scope: changing BRC-100 method contracts; refusing otherwise fundable actions; globally prohibiting unproven or sending parents; eager one-shot consolidation; changing custom operator basket values; publishing packages or deploying images from this PR.
  • Exact head SHA reviewed: 7390ce94ae00ccb796743c9f83cfe256ebdb9795

This PR implements the root-cause correction as one coherent policy. It preserves every historical funding avenue as a compatibility fallback, but biases future authorized activity toward a useful, parallel-ready managed-change pool. Existing wallets migrate progressively as they naturally spend old outputs; no background sweep, consumer migration, or new authorization is required.

Behavioral invariants

  1. Never introduce a new funding refusal. Candidate selection first tries the new shape and then the historical shape within each ancestry tier. After settled and unproven liquidity are exhausted, sending parents remain available. The final compatibility attempt uses an economic floor of one satoshi for its first remainder, which is at least as permissive as the historical 32-satoshi/8-output planner.
  2. Prefer robust ancestry without withholding liquidity. Immediate actions prefer completed, then unproven, then sending change. Pending parents are a last-resort funding source for normal plans. If an all-settled plan has already crossed the configurable 16-input comparison threshold, the planner may also measure later tiers and choose them only when the exact transaction-plus-BEEF serialization is smaller.
  3. Do not grow the pool merely to hit a count. Change shaping starts only from the action's real post-output, post-fee surplus. It never gathers extra inputs solely to create more managed outputs.
  4. Bound fanout and cleanup. A normal action creates at most eight managed-change outputs and may consume at most four additional fee-positive legacy fragments. Both values are operator-configurable and accept -1 for unlimited operation.
  5. Avoid uneconomic fragments. The default basket targets 144 outputs with a preferred value of 5,000 satoshis. Smaller remainders are still retained when necessary to preserve spendability; 5,000 is a liquidity preference, not a dust rule or a reason to fail.
  6. Preserve privacy behavior. Surplus continues to use the existing Benford-derived distribution rather than fixed equal denominations. The policy changes the useful-value target and work bounds, not the privacy model.
  7. Preserve concurrency safety. Input status is carried with each candidate tier and revalidated during the atomic claim. A candidate cannot become eligible merely because its ancestry changed between planning and reservation.
  8. Keep delayed operations fast. Durable single and grouped permission grants again use delayed broadcast. This avoids forcing a permission prompt to wait for mining/broadcast settlement while retaining normal authorization and persistence semantics; grouped permissions remain allow/deny decisions, with no invented grouped “Once” state.

The final adversarial review identified and this head closes two pre-merge blockers. First, actions already funded by explicit/fixed inputs now materialize managed change from that existing surplus before the compatibility guard. A 7,000-satoshi input funding a 1,000-satoshi output creates a 5,999-satoshi remainder without calling the wallet allocator; larger surplus can fan out within the eight-output cap, and optional fragment retirement remains independently bounded by the four-input migration budget. If the remainder cannot pay both the marginal output fee and the economic dust floor, it remains as a bounded fee rather than causing a retry that gathers another input solely to manufacture change. Second, the SQL policy migration now writes UTC ISO timestamps on SQLite, preserving lexical compatibility with incremental-sync cursors, while MySQL retains its native millisecond timestamp expression.

Planner and BEEF behavior

The funding planner builds progressively broader candidate sets:

  1. completed parents only;
  2. completed plus unproven parents;
  3. completed, unproven, and sending parents.

Each tier gets a new-policy attempt followed by a same-tier compatibility attempt. This ordering matters: a fragmented settled wallet gets every chance to proceed before pending ancestry is considered, while a wallet that can only proceed through a pending parent still proceeds exactly as before.

For a settled plan over pendingComparisonInputs (default 16), the planner compares the serialized transaction size plus the exact serialized input BEEF bytes for the later candidate. The smallest measured plan wins. A comparison-only proof lookup failure gives that alternative infinite comparison cost and leaves the already viable baseline untouched. -1 disables the optional comparison without disabling pending fallback.

This directly reduces the class of 100+ input permission and application transactions while avoiding a blanket “never use sending” rule that would turn temporary confirmation delay into an outage. It also reduces the blast radius of a failed ancestor broadcast: pending ancestry is used deliberately, after settled liquidity or based on a measured total-payload improvement, rather than because database ordering happened to return it first.

Progressive migration for existing wallets

The historical untouched default is recognized narrowly as:

  • basket name default;
  • target count 144;
  • minimum desired value 32 satoshis.

Only that exact default advances to 5,000 satoshis. Custom baskets and any operator-modified values are left untouched.

The transition is applied consistently through:

  • a Knex migration for SQL-backed wallets;
  • IndexedDB schema/open migration for browser wallets;
  • portable restore and sync normalization, so an old snapshot cannot silently reinstall the obsolete default.

The migration changes policy metadata only. It does not create a transaction or reserve funds. Subsequent authorized createAction calls consume old fragments within the configured migration budget and create useful change from real surplus. This makes the transition gradual, interruptible, and compatible with wallets that are only intermittently active.

SQLite migration timestamps use the same UTC ISO representation as its incremental-sync query values, so a newly migrated basket is visible in the first eligible sync chunk. MySQL continues to use its native timestamp representation. Regression coverage proves both the exact-match migration and SQLite text comparison from a cursor captured immediately before migration.

Action batches and multiple workspaces

The #448 isolation model remains authoritative: batch membership is determined by explicit transaction-graph references rather than a global “batch mode.” This PR aligns reservation planning with the same ancestry and managed-change rules:

  • completed outputs are reserved before unproven and sending outputs;
  • providers that do not return the new optional status field are treated conservatively as last-resort pending candidates;
  • new preferred values and shaping/migration limits are returned as optional capability data;
  • compulsory funding remains governed by economic dust, not by the 5,000-satoshi preference;
  • compatibility fallback remains capable of funding a batch when the preferred shape cannot;
  • ordinary createAction work and independent action-batch workspaces are not claimed merely because another workspace exists.

All additions to storage candidates and batch capability/results are optional/additive. A custom or older provider that lacks the status batch API continues through the base fallback, and no Wallet Wire, Storage Server RPC, persisted action, or BRC-100 request/response contract is narrowed.

Monitor and operator visibility

TaskReviewUtxos now reports managed-pool condition without mutating it: total managed outputs, useful/undersized counts and satoshis, ancestry-status counts, and current policy values. The authenticated storage admin endpoint/UI exposes the same report. No identity keys, scripts, outpoints, transaction IDs, or other wallet-sensitive values are added to telemetry.

The official wallet-infra image accepts and validates:

  • WALLET_STORAGE_MANAGED_CHANGE_MAX_OUTPUTS_PER_ACTION (default 8)
  • WALLET_STORAGE_MANAGED_CHANGE_MIGRATION_INPUTS_PER_ACTION (default 4)
  • WALLET_STORAGE_MANAGED_CHANGE_PENDING_COMPARISON_INPUTS (default 16)

Every limit accepts -1 with the documented meaning. The values are present in the example environment, Docker Compose wiring, Kubernetes ConfigMap sample, service-operation governance inventory, and operator documentation. Invalid values fail startup instead of producing an ambiguous partial policy.

Economics behind the defaults

At the Wallet Toolbox default fee model of 100 satoshis/kB, a roughly 148-byte managed input contributes about 15 satoshis of fee, and a minimal one-input/one-output spend is about 20 satoshis. A 5,000-satoshi unit is therefore about 250 minimal-spend fees at that rate. Even at 1,000 satoshis/kB it remains about 26 minimal-spend fees. A fully aligned 144-output pool represents approximately 720,000 satoshis, while wallets below that balance simply converge on fewer useful units rather than being forced to create dust.

These are operational defaults, not consensus constants. The 144 target provides enough independent outputs for parallel planning; the eight-output fanout avoids a large one-time transaction; and the four-input migration budget prevents cleanup from recreating the very 178-input pattern this work is intended to eliminate.

Impact

  • No public package source or manifest changed
  • Public package source or manifest changed; affected packages are listed below
  • Infrastructure source, dependency, image, or deployment configuration changed
  • Public API, exports, types, runtime targets, or browser/mobile behavior changed
  • Security-sensitive boundary changed
  • Documentation or examples changed

Affected packages/services and intended patch versions (publication occurs only through the release workflow after approval):

  • @bsv/wallet-toolbox: 2.6.5 published -> pending 2.7.0
  • @bsv/wallet-toolbox-client: 2.6.5 published -> pending lockstep 2.7.0
  • @bsv/wallet-toolbox-mobile: 2.6.5 published -> pending lockstep 2.7.0
  • official wallet-infra image: consumes the released 2.7.0 package after the protected version-sync/release cascade

The new public surface consists of additive policy configuration/types, optional candidate ancestry status, optional action-batch policy information, and read-only admin reporting. Existing calls remain source- and wire-compatible.

Verification

  • Local commands and results:
    • pnpm health:check — 138 repository/governance policy tests passed.
    • pnpm lint — passed.
    • pnpm format:check — passed.
    • governed workspace build — all workspace projects passed, including the docs site.
    • pnpm typecheck — all 37 TypeScript projects passed after the governed build.
    • pnpm audit:security — passed; two existing high advisories are covered by the repository's governed exceptions, with no new dependency graph.
    • pnpm docs:examples — 8 examples passed against 21 exact package tarballs.
    • Wallet Toolbox full Jest suite — 196 suites, 1,796 tests passed; one existing test skipped.
    • Wallet Toolbox full coverage suite — 196 suites, 1,796 tests passed; one existing test skipped.
    • Wallet Toolbox build, lint, and packed CommonJS consumer verification — passed.
    • Wallet Toolbox Client build, 17 tests, and ESM/CommonJS packed-consumer verification — passed.
    • Wallet Toolbox Mobile build, 19 tests, mobile coverage, and ESM/CommonJS packed-consumer verification — passed.
    • wallet-infra standalone install (npm ci --ignore-scripts, zero vulnerabilities), build, lint, and changed-file formatting — passed.
    • patch-coverage gate — 92.38% (582/630 changed line/branch points), above the repository 90% requirement.
    • patch-coverage harness regression suite — 5/5 passed, including fail-closed behavior and exact non-emitting declaration/barrel handling.
  • Hosted CI: exact head 7390ce94ae00ccb796743c9f83cfe256ebdb9795 is terminal green: 42 checks passed and six scope-based jobs skipped as expected. The repository-owned merge gate, zero-new-findings Sonar gate, CodeQL, Codecov patch gate, Socket checks, Conformance, container runtime contracts, Wallet Toolbox coverage shards, wallet browser/mobile consumers, docs, dependency review, and wallet-infra image/security contract all passed. Evidence: CI, CodeQL, Conformance, and container runtime contracts.
  • Conformance evidence: deterministic funding, action-batch reservation/extension, SQL and IndexedDB migration, portable restore, storage remoting, admin formatting, Monitor reporting, permission settlement, candidate ancestry, fallback-provider, and operator-config parsing tests are included.
  • Coverage delta: new tests exercise bounded surplus fanout, fee-positive fragment migration, small balances/remainders, fixed-input surplus without wallet allocation, fixed-input migration-budget enforcement, sub-dust fixed-input surplus, SQLite incremental-sync visibility, all -1 modes, status-tier fallback, sending-only liquidity, exact BEEF comparison, comparison-proof failure, custom-provider status enrichment and unresolved-status failure, atomic status validation, legacy custom-policy preservation, action-batch compatibility, and progressive migrations. The current exact head covers 582/630 changed line/branch points (92.38%).
  • Lint/typecheck delta: zero new warnings or type errors.
  • Browser/mobile/packed-consumer evidence: all three lockstep artifacts build and validate in their supported module modes; the mobile platform suite and source coverage pass.
  • Performance or bundle-size delta: the common settled path performs no pending comparison below 16 inputs. Large fragmented plans may perform bounded alternative planning in exchange for a smaller measured transaction/BEEF payload. Permission persistence retains delayed broadcast and therefore does not inherit the latency regression proposed by grouped settlement workarounds. No dependency or bundle-graph change was introduced. Exact packed artifacts add about 0.5–1.0% for the shared runtime policy: Vite 1,557,196 raw / 365,526 gzip / 287,329 Brotli bytes; esbuild 1,216,272 / 334,453 / 268,547; Metro 1,616,960 / 408,290 / 317,146; Hermes 3,266,887 / 1,315,512 / 1,023,813. Linux CI observed 1,558,352 Vite raw, 1,218,452 esbuild raw, and 3,271,396 Hermes raw bytes. Browser ceilings and the Hermes raw ceiling advance with narrow evidence-backed headroom; Metro and compressed-mobile ceilings remain unchanged.
  • I self-reviewed the complete diff for correctness, security, compatibility, public API, artifacts, dependencies, docs, and operations
  • All applicable checks are terminal and successful on the exact head; any scope-based skip is expected and validated by the merge gate

Security and dependencies

  • No dependency or lockfile change
  • Changelog, runtime relevance, peer compatibility, transitive graph, and audit results were reviewed
  • CodeQL/negative tests cover any changed trust boundary
  • The exact-head CodeQL analysis has no new alert
  • The exact-head repository quality gate reports zero new Sonar findings (including accepted or false-positive issue states) and zero unreviewed hotspots; Sonar's aggregate Quality Gate passed verdict alone is not merge evidence
  • No new override, advisory dismissal, quality suppression, or skipped test
  • Any temporary exception is registered with owner, evidence, review date, and removal condition — not applicable; none added
  • Workflow permissions and lifecycle-script behavior remain least privilege

Candidate ancestry is treated as authorization-adjacent state: it is advisory during planning but authoritative at the atomic claim. Status changes cannot cause a planner to claim an input outside the selected ancestry set. Comparison/proof retrieval is fail-safe toward the existing viable baseline. Operator admin reporting remains behind the existing authenticated/allowlisted boundary and exposes aggregates only.

Dependency evidence

For human-authored dependency changes, add the useful evidence available for the review. Missing or incomplete fields produce an advisory CI warning, not a merge block. Automated dependency pull requests are exempt; their generated release, compatibility, and security metadata remains the review starting point.

  • Release notes and necessity: no dependency change in this PR. Governance release notes now combine the TTN minor candidate from current main with the complete managed-liquidity behavior, rather than publishing fix(wallet-toolbox): isolate and resume action batches #448 and this correction as incompatible partial policies.
  • Runtime, build, and peer compatibility: Node 24 governed build/typecheck passes; client/mobile packed consumers pass; storage wire additions are optional.
  • Deduplicated lockfile: unchanged.
  • Audit and CodeQL: local governed audit passes with the existing registered exceptions; exact-head hosted CodeQL is green with no new alert.
  • Package and consumer tests: wallet 1,796 tests; client 17; mobile 19; all pass, plus packed-artifact validation.
  • Bundle and performance impact: no dependency graph change; bounded fanout lowers output/BEEF growth, settled fast path remains direct, and pending comparison is thresholded/configurable.
  • Affected public package versions: wallet-toolbox, wallet-toolbox-client, and wallet-toolbox-mobile 2.7.0 lockstep release.

Release and operations

  • No npm publication was performed from a workstation or from this PR
  • Required npm version bumps are included or intentionally deferred by the controlling program — current main advances the combined lockstep candidate to 2.7.0 for TTN; npm still reports 2.6.5 as published
  • Image/SBOM/provenance/deployment/rollback impact is documented
  • Documentation, changelog, migration, and operational guidance are current

After merge, the protected release workflow publishes the three 2.7.0 wallet packages in lockstep. Its version-sync step updates the standalone wallet-infra dependency/lock before the official image is built; this PR deliberately does not make CI depend on an unpublished npm version. Operators can roll out with defaults, observe the read-only liquidity report and transaction/BEEF telemetry, and then tune limits if their workload warrants it.

Until that sequence completes, the source candidate's standalone wallet-infra lock still resolves Wallet Toolbox 2.6.5. The new environment variables are parsed and validated there but are operationally inert because 2.6.5 does not yet consume the policy object. Deployments must verify that official image provenance contains Wallet Toolbox 2.7.0 or newer before relying on those settings.

Rollback is application/image rollback plus policy restoration. The SQL policy migration is intentionally one-way because later authorized actions may already have reshaped funds; rolling back code does not and must not attempt to reverse blockchain transactions. Existing 5,000-satoshi outputs remain ordinary valid managed change under older code.

Documentation delivered

  • full managed-change algorithm, invariants, economics, concurrency model, and migration guide;
  • pending-parent/BEEF comparison and failure semantics;
  • action-batch integration and compatibility behavior;
  • operator configuration and every -1 mode;
  • Monitor/admin interpretation and rollout/rollback checklist;
  • wallet, browser, and mobile API migration notes;
  • wallet-infra environment, Compose, Kubernetes, service operations, resource guidance, changelog, and governed release notes.

Completion evidence

  • The linked tracker is updated only for work fully proved by merged code, passing checks, resolved alerts, measurements, or an approved exception
  • Review conversations are resolved
  • Documentation, changelog, migration notes, release notes, and operator guidance are current or concretely not applicable
  • No pending, failed, stale, cancelled, or unexpectedly skipped check is being handed to another contributor as “complete”
  • One qualified maintainer approval is sufficient; no last-pusher restriction is assumed

The remaining unchecked items depend on tracker disposition and maintainer review; they are not represented as complete while those gates remain open.

@ty-everett
ty-everett marked this pull request as ready for review August 11, 2026 02:30
…uidity-policy

# Conflicts:
#	docs/reference/package-api-migrations.md
#	governance/package-release-notes.json
#	packages/wallet/wallet-toolbox/CHANGELOG.md
#	scripts/patch-coverage.mjs
#	scripts/patch-coverage.test.mjs
@sonarqubecloud

Copy link
Copy Markdown

@BraydenLangley
BraydenLangley merged commit a7f830d into main Aug 11, 2026
48 checks passed
@BraydenLangley
BraydenLangley deleted the codex/wallet-utxo-liquidity-policy branch August 11, 2026 15:37
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