Trust Local, But Verify: How Henceforth Manages UTXOs and Transaction History

The wallet's source of truth is the local view of the chain that we've already verified. The network's job is to hand us new evidence; our job is to integrate that evidence additively — never to clobber a known-good stored balance because some service had a momentary stale view.

Henry Hudson · Henceforth · 2026


1. The invariant

Satoshi's framing from §2 of the white paper:

A transaction once included in a block is a fact. Silence from a service is not its negation. Funds change as a consequence of new facts arriving, never as a consequence of old facts going missing from a response.

This is the invariant the wallet rests on:

  1. Funds can change. A UTXO can be spent. New UTXOs can appear.
  2. Transaction history cannot be retracted. Once a transaction is observed and verified, the only further evidence the chain can produce about it is a deeper confirmation.

Any wallet design that's allowed to shrink the stored balance because the network response was incomplete has, by construction, broken (1) and (2) — it's letting the network's silence stand in for a fact, when only the network's speech should count.


2. The underlying mathematics

Trust local, but verify: how is that even possible? The sentence collapses to wishful thinking unless the wallet can independently check the claims it gets from a chain service. Three cryptographic primitives carry the weight, and together they pin the answer to mathematics rather than authority.

Signatures

Every transaction input is a (previous outpoint, unlocking script) pair. For a standard P2PKH output — the script type this wallet produces — the unlocking script carries two items: an ECDSA signature over the transaction's commitment hash (the sighash), and the public key itself. The previous output's locking script does not store that public key; it stores only its hash. Verification is therefore two checks chained together: the supplied public key must hash to the value the locking script committed to, and the signature must verify against that public key. Both directions of the math are one-way — producing the signature requires the private key; verifying it requires only the public key.

WhatsOnChain cannot produce a signature for an outpoint whose locking script commits to a public key only the user's wallet can derive. The chain itself rejects any transaction whose signatures don't verify; the wallet rejects any candidate transaction whose math doesn't hold; neither needs WoC's permission to do so.

Merkle proofs

A block's merkleroot is a 32-byte commitment to the entire transaction set in that block, computed via repeated SHA-256 hashing pairwise up a binary tree. A TSC merkle proof (BRC-74) is the list of sibling hashes along the path from a transaction's leaf to the root. Given the txid, the proof, and the root — three values, all locally checkable — anyone can recompute the path and compare. Either it reconstructs or it doesn't.

WoC supplies the proof. The wallet verifies it against the merkleroot of a header the wallet itself has validated. WoC cannot forge a proof that lands a transaction in a block it didn't really land in, because every reconstruction step is a SHA-256 hash and the final result is checked against bytes the wallet already trusts.

A worked example — verifiable by the reader. Take transaction bc18c836…8663d348, the 101st transaction (index 100) of mainnet block 950,000. WhatsOnChain's TSC-proof endpoint returns its inclusion proof — ten sibling hashes and nothing else:

{
  "index": 100,
  "txOrId": "bc18c83691f6367aa5e8be52296a82350da19370dd0bb921fe7cf8748663d348",
  "target": "00000000000000000720b9f711716317effd01a2935921056c46da7b28e40dda",
  "nodes": [
    "98bf0dbfea77ba59011f74267df75b856ac0b501e9030bbb29daa3a853c45851",
    "7bbd769c41819b953fb8bd593a343e216f20299054f89bf9debbdad8fd2ac6d1",
    "0003e6231b2104cca3bc6ba31e3d4436c39a837e806f0a8326a011b9c14ce7dc",
    "1d248453fb780c30f19f627775b73d9495ee7f789535fbdb1be178a37fbd8699",
    "489a7c178342ec5dcd3caed8ec9d0576b9517b28a5b071f7a839d58442ba1b8b",
    "a3f2b00f29c8ff1905058a55d74837f2021bf3f69ed13ffe4bc780a017d969de",
    "6938396581bc845cefd92b57001a79849d4a2efeb091078a3fb13fea222724be",
    "05a318f364e69b2e9b9d60a74c146a182369178babfd429266ec274f85001b87",
    "0d377023ff21b041f28ae25c0a4cd2aa4d13224d2ca14f3e2b196a1bc300568f",
    "b09ba7180f915b539523c26d9b5d47952c49a1bdd73adbd8b2eec587ca29abc4"
  ]
}

Ten hashes because block 950,000 holds 514 transactions, and a binary tree over 514 leaves is ten levels tall. The index does double duty: it is both the leaf's position and the turn-by-turn directions up the tree. Written in binary, 100 = 0b0001100100; read from the least-significant bit, each bit says whether the running hash is the left child of its pair (bit 0 → concatenate running || sibling) or the right child (bit 1sibling || running). Each pair is then folded with H(x) = SHA-256(SHA-256(x)) — the same double hash proof-of-work relies on below:

start    bc18c836…   the txid, in internal byte order
lvl 0   bit 0   H(running || node0)   → d944e7fe…
lvl 1   bit 0   H(running || node1)   → 098f0027…
lvl 2   bit 1   H(node2 || running)   → df184b85…
lvl 3   bit 0   H(running || node3)   → a54e9476…
lvl 4   bit 0   H(running || node4)   → 409e7890…
lvl 5   bit 1   H(node5 || running)   → d7632bdd…
lvl 6   bit 1   H(node6 || running)   → a31e7fed…
lvl 7   bit 0   H(running || node7)   → c11c96d7…
lvl 8   bit 0   H(running || node8)   → a8743f50…
lvl 9   bit 0   H(running || node9)   → 4d3ec209…

The hash that falls out of level 9 is 4d3ec209e8e881aa49672864cf6794414df7028c59dddf0b80406d99baf32fa7 — and that is exactly the merkleroot field of block 950,000's header. Ten double-SHA-256 operations, no network calls, no trusted third party consulted. The proof's target names the block; the wallet looks that block up in its locally-validated header store and reads the merkleroot to compare against — which is why verification rests on a header chain the wallet built for itself, not on WhatsOnChain's word.

Had WhatsOnChain altered one bit of the transaction, or substituted a sibling hash from another block, the reconstruction would terminate on a root that does not match the header the wallet already holds — and the answer would be discarded. The reader can run this too: the txid, the block, and the ten nodes are all public, and the recomputation is a dozen lines in any language with SHA-256. SPVService.verifyTransaction does exactly this on-device for every /tx the observed-spend pass fetches.

Proof of work

Every block header includes a nonce and a difficulty target encoded in bits. The header is valid iff SHA-256(SHA-256(header)) interpreted as a little-endian integer is less than the target. Finding a nonce that satisfies the inequality takes (on current mainnet) on the order of 10²⁰ hash operations — a quantity whose dollar cost is the entire raison d'être of the mining industry.

The wallet validates the PoW on every header it integrates. WoC cannot forge a header because doing so would require redoing the work — work that takes mining hardware days to weeks per block, not a stale replica's milliseconds.

Our own transactions: WoC is told, not asked

When the user broadcasts a transaction from this wallet, the resulting outputs are known by construction. The wallet built the locking scripts, computed the txid, derived the change address, signed the inputs. The change UTXO is added to local state at sign time — before broadcast, before any network confirmation. We don't ask WoC whether the outpoint exists; we tell the chain it exists, and ask only that the chain confirm it.

That confirmation, when it arrives, is the merkle proof. The proof verifies inclusion locally (same Merkle reconstruction as above), promotes the local UTXO from "pending" to "confirmed", and advances the watermark. WoC never tells the wallet anything new about an output the wallet itself authored — it tells the wallet when the output finally appears in a block, which is information the wallet already half-knew (it had the bytes; now it has the inclusion proof too).

This is why self-broadcasts and external broadcasts traverse different code paths. A self-broadcast doesn't go through the observed-spend pass; SubmitTransaction.swift adds the change output to unspentTransactions directly, before any network refresh. External broadcasts — a payment to one of our addresses by a peer — have to surface through /history because we couldn't possibly know about them otherwise. Both end up verified by the same Merkle reconstruction. Only one of them ever required WoC to be the source of the information.

What this reduces WoC's role to

These primitives give the wallet a strict rule: trust nothing about a transaction until the math says it's verified. WoC's role narrows to two things — handing us candidate answers we wouldn't otherwise know to ask about (which addresses have new activity, which TXIDs to look up), and providing the merkle proofs we need to verify those candidates. Both are convenience layers.

The cryptographic kernel — signature verification, Merkle reconstruction, PoW check — runs on the device, against bytes only the user's local state vouches for. WoC can rate-limit us. WoC can give us stale data. WoC can in principle lie outright. The wallet's UTXO state advances only when the math says it can.

This is what makes "trust local, but verify" precise rather than aspirational. The wallet does talk to WoC. It just doesn't trust what WoC says — it uses what WoC sends as input to a verification step the wallet can fail. When the verification fails the answer is silently dropped; when it passes the answer was redundant — it was already true mathematically, WoC just happened to be the messenger. The rest of this article is the engineering scaffolding that holds this property up across rate limits, partial failures, replica lag, and cross-device sync. The math is the floor; the rest is plumbing.


3. The bug class we closed

Before May 2026, Henceforth had a balance-reconciliation enum with two modes:

enum BalanceReconciliationMode {
    case authoritative   // computed = new truth → overwrite stored
    case lowerBound      // max(stored, computed) → never shrink
}

The launch path, the WoC sweep, the post-broadcast path, the BRC-100 internalisation path — every one of them passed a mode. Some passed .authoritative when "all addresses queried successfully" was true. The shape was reasonable on paper: if every address came back cleanly, the sum is the new truth.

The shape doesn't survive contact with a real chain service. WhatsOnChain has rate limits, mempool-propagation lags, and rare moments where one of its replicas is behind the others. "Every address came back successfully" turns out not to be the same statement as "every address returned the same view of the chain that the chain itself contains." A successful HTTP response with a stale snapshot reads as truth; .authoritative overwrites; confirmedSatoshis drops; the user sees their balance bounce 69 → 32 → 41 on launch and refresh.


4. Two writers, two semantics

The May 2026 refactor deleted the enum. In its place: two methods with explicit names that force every caller to declare what it trusts.

MethodSemanticUse from
reconcileWalletBalanceFromUTXOsmax(stored, computed) on confirmed sats — a floor; never reduces stored. Unconfirmed sats assigned authoritatively (see note below).Any network-fetch or disk-load path. Safe by construction.
setWalletBalanceFromArrayOverwrite from sum. Trusts the live UTXO array.Code that just modified the local UTXO array itself — post-broadcast, observed-spend, BRC-100 action internalisation.

The split is the architectural commitment. The floor method can be called from anywhere without thinking; misuse is impossible — the worst case is the stored balance stays where it was. The overwrite method requires the caller to be able to justify, in code, why the array it's looking at is the result of an action it controls. The method name and docstring make that justification load-bearing.

Where each call site goes after the refactor WoC /unspent sweep Disk load (preloadWalletData) WalletView reload Post-broadcast (Submit/Tx) Observed-spend pass BRC-100 internaliseAction reconcileWalletBalanceFromUTXOs max(stored, computed) — floor setWalletBalanceFromArray overwrite from sum Stored balance never reduces Stored balance matches array
Five call sites, two semantics. Misuse is hard because the method names won't let a caller forget which one it's calling.

5. The watermark

Two writers handle how the stored balance updates. WalletSyncWatermark handles what we've verified, and when. It's a per-wallet value type that rides the wallet's existing primary-address-keyed iCloud Drive file — cross-device sync of verification state is a free side-effect.

WalletSyncWatermark — the trust frontier lastVerifiedBlockHeight: Int Highest block height the most recent fully-successful pass observed. Advances atomically when every queried address returned cleanly. addressBlockHeights: [String: Int] Per-address ceiling. Each entry advances independently on its address's successful pass. Soft per-address policy — one bad address doesn't hold the others back. lastVerifiedAt: Date Strict signal — advances only when every queried address succeeded.
Three fields. The first two accumulate evidence; the third is the strict signal that PayView's staleness banner reads. lastVerifiedAt advances only when an entire pass returned cleanly — partial success keeps per-address progress but holds back the “fully verified” timestamp. The delta query's “already integrated” skip set is deliberately not a watermark field — see below.

Merges across devices follow the additive-only convention used elsewhere in the wallet's iCloud sync: max on heights, max on timestamps. Two devices advancing concurrently converge rather than clobber. Whichever one writes second does existing.merged(with: localCopy) and the result is no worse than either contributor.

The delta query's skip set — the txids the wallet has already integrated and need not refetch — is deliberately kept out of the watermark. It's derived per-device at sweep time from myTransactions, this device's own record of transactions it has fetched and input-scanned. A cross-device-merged txid set would be actively unsafe here: a spend processed on one device would land in the shared set, and a second device would then skip the very transaction whose inputs it needed to scan to notice its own UTXO had been spent — a phantom-balance bug. So the rule splits cleanly: verification frontier (heights, timestamp) syncs across devices; the did-I-already-look-at-this bookkeeping stays local, because only the local device knows what it has actually scanned.


6. The three-phase launch sequence

The launch coordinator runs three phases in order — and Phase 3 commits to one of two terminal states. The same iCloud-merge step that begins the launch sequence also runs on every visit to the wallets list (WalletsView's .onAppear) — every wallet interaction in the app originates from that view, so all downstream screens inherit a fresh local baseline before any network call. The in-wallet refresh button consults the network directly without re-running the full launch sequence; it relies on the WalletsView gate having merged iCloud on the user's most recent navigation through the list.

loadingICloud iCloud baseline verifyingDeltas /history pass .live every address ✓ .staleNoNetwork partial / failed Watermark saved to iCloud
Phase 1 (iCloud) blocks; the user sees “Syncing iCloud data…”. Phase 2 (deltas) is non-blocking; the chip says “Checking for updates…”. Phase 3 commits the watermark atomically and transitions to .live on full success or .staleNoNetwork on a partial pass — the latter preserves the prior lastVerifiedAt so PayView's banner reads “last verified N hours ago” instead of “never.”

7. Removal by evidence, not by absence

The hardest property to preserve under a rate-limited chain service is: do not delete a UTXO from local state unless the chain itself has shown us evidence the UTXO is spent. The pre-refactor wallet's conservative merge heuristic — "if we queried this address and the UTXO didn't come back, remove it" — fails this test exactly when WoC has a stale view of one of its replicas.

The architectural fix is to read forward, not to interpret silence. For every address where we currently hold UTXOs, the verification pass in wocGetUnspentTransactions.swift walks /history, subtracts the txids it has already integrated (derived per-device from myTransactions), fetches /tx/{txid} for what's left, and scans the inputs. An input whose outpoint matches one of our UTXOs is provably spent. Anything else is silence — and silence isn't evidence.

For each address where we hold UTXOs GET /history subtract integrated txids For each new TXID GET /tx/{txid} Scan tx.vin match outpoints Match → remove UTXO setBalanceFromArray No match → no-op balance unchanged No "address queried but UTXO missing" heuristic. Evidence in, removal out.
The observed-spend pass turns “transaction history is append-only” into a removal rule. The only way a UTXO leaves unspentTransactions via the sync path is by appearing in the inputs of a transaction we've fetched and integrated. Stale WoC responses produce no removal — they produce a no-op until a future pass catches up.

This is what makes external broadcasts detectable. If a peer device, a watch-only on another platform, or another wallet rooted at the same seed spends a UTXO, the spending transaction appears in /history for the address we share. The pass fetches it, finds our outpoint in its inputs, removes the UTXO, and recomputes the balance via setWalletBalanceFromArray — legitimately, because the removal came from chain evidence, not from missing-in-response.

A second tightening, shipped after the initial article: every fetched /tx/{txid} is now SPV-verified against the locally-validated header chain before its inputs scan against our outpoints. An unverifiable transaction (a mempool sighting with no merkle proof yet, or a tx whose claimed block is above validatedTipHeight) is still recorded in myTransactions so the next sweep skips refetching, but produces zero UTXO removals. The "trust the answer, not the answerer" property — once §11's acknowledged gap — is now literal: a WoC /tx response that doesn't reconstruct a valid Merkle proof against our headers carries no authority to change the wallet's state.


8. The white paper alignment

The architecture lines up against three sections of the Bitcoin white paper:

§StatementImplementation
§2An electronic coin is a chain of digital signatures.UTXO state is derived from transactions, not from a service's summary. setWalletBalanceFromArray trusts the array because the array's contents are signed chain bytes the wallet itself integrated.
§5Each node broadcasts new transactions to all nodes.Broadcast is the network's mechanism, not a precondition of the wallet's correctness. The sender's local view stays trustable even when the network surface is rate-limited or unreliable.
§8Simplified Payment Verification — a user can verify by keeping a copy of block headers.The header sync service maintains a chain-linked local header store anchored to a hard-coded checkpoint. The observed-spend pass's /tx/{txid} fetches are merkle-verifiable against those headers. Trust the answer, not the answerer.

9. The Wright alignment

Four Wright pieces bear directly on the design. Each is named, the load-bearing claim is paraphrased, and the implementation that carries it is pointed to.

A. The Home Node That Never Validates

Wright's distinction between validation (block creation, performed by miners under proof-of-work cost) and verification (local checking, performed by anyone who keeps a copy of the headers) is the vocabulary the architecture rests on. Henceforth is a wallet, not a node. It contains no mining code, constructs no blocks, performs no proof-of-work. What it does is verify: it keeps a locally-validated header chain, fetches transactions by hash from chain services, and checks their inclusion via merkle proofs against headers anchored in a hard-coded checkpoint. The watermark literalises this: lastVerifiedBlockHeight records what we've checked, not what we've enforced.

Wright's framing of local rejection as exit, not command shows up directly in two places. The PayView staleness banner doesn't "veto" anything — it tells the user the device's view may have drifted from chain reality, and offers the user a way to refresh. The phase machine's .staleNoNetwork state similarly degrades trust locally without ever pretending to control miners. Both are exit signals, in the home-node article's sense.

ClaimImplementation
Wallets verify; they do not validateSPVService, HeaderSyncService; no mining code anywhere in the codebase
The mempool is a local waiting room, not a law courtmyTransactions is local state; MempoolMonitorService is a polling layer, not a global view
Direct paths to miners are the route that mattersARC broadcast (GorillaPool with TAAL failover) — direct miner submission, no p2p relay dependence
Local rejection is exit, not commandThe staleness banner, the phase pill, the watermark itself — informational, never coercive

B. Small Worlds, Large Errors

Wright's four-layer model — block creation, transaction access, block propagation, local verification — gives the wallet's role a precise place. Henceforth lives in layer 4 (local verification) and uses layer 2 (transaction access) for delivery, via ARC. Layers 1 and 3 belong to miners and pools; the wallet doesn't pretend to occupy them.

The architectural payoff of refusing to occupy layers 1 and 3 is honesty. A wallet that pretends to validate (in Wright's productive sense) acquires a phantom responsibility — code paths that exist to honour an authority it doesn't actually have. Henceforth doesn't carry that weight. Every behaviour the wallet displays is something it can mechanically justify with a verification pass, a merkle proof against locally-validated headers, or a watermark advance. None of the architecture's correctness assumptions depend on the public-gossip topology being healthy. Broadcast goes through a miner-facing hub (ARC). Header sync rides a stream from a chain-indexing service (WhatsOnChain over WSS) whose data the wallet still merkle-verifies locally before promotion. The transaction-history delta walks /history on the same chain-indexing service, but again — the wallet trusts the merkle-verified inclusion, not the service's say-so about a TXID's existence.

Wright's four-layer model → Henceforth components Layer 1 — Block creation (miners) Henceforth has no role here Layer 2 — Transaction access ARCService → GorillaPool / TAAL (direct miner submission) Layer 3 — Block propagation (miners) Henceforth has no role here Layer 4 — Local verification HeaderSyncService · SPVService · WalletSyncWatermark
The wallet occupies the two layers it can honestly own. The miner layers are not pretended at. The trust the user places in the wallet is precisely the trust we can earn at layers 2 and 4 — direct submission for sending, locally-verifiable proofs for receiving.

C. The Myth of the Sovereign Node

Wright's piece is a corrective against the BTC-Core-era conflation of running validation software with governing the network. The architectural implication for a wallet is small but specific: the wallet's stored state is an artefact of what this device has verified for this user, never a claim about the chain at large. The watermark's vocabulary follows: lastVerifiedBlockHeight, addressBlockHeights, lastVerifiedAt — all three fields scope to the wallet, never to the chain. A peer device may have a different watermark, and that's not a disagreement about reality; it's two devices reporting their own verification frontiers, which converge via the additive merge described in §5.

D. Batching, Headers, and Throughput

The technical specification the rest of the wallet rests on. Wright frames three pillars — header-based SPV awareness, batch construction, and structured UTXO management for both directions of value flow. Three of his specific architectural choices are worth pulling out because they do load-bearing work that isn't obvious until you've tried to build the wallet without them.

Offline header transfer (§III) lives in HFHeaderBundle. Wright's air-gap workflow assumes the online machine periodically batches block headers and hands them to the offline machine via removable media. Henceforth implements this for the two-iPhone Cold Mode case: the online phone exports its validated header run as an .hfheaders AirDrop bundle, the offline phone imports it, and the import path runs the full chain-link + PoW + checkpoint-anchor validation against the bundle before promoting any of it into the local store. The trust anchor is not "AirDrop preserved the bytes" — it's that the offline machine's checkpoint hash matches what the bundle chain-links back to. Bytes that don't anchor get rejected at the import boundary, not after.

Fragmentation-aware UTXO selection (§V) lives in Wallet.selectUTXOs. Wright observes that micropayment-heavy systems accumulate hundreds or thousands of small UTXOs, and any selection policy that ignores fragmentation produces transactions that fail by size before they fail by economics. Henceforth's policy switches when the wallet exceeds 100 UTXOs or when 40+ UTXOs each average under 5,000 sats: the dust cap raises from 10 to 50, the sort order flips from largest-first to smallest-first, and smart-change splitting disables. The 100KB standard-tx ceiling throws txTooLarge before biometric authentication, so the user never approves a payment the network will reject.

"The send is the consolidation event" (§V continued). The cleanest piece of Wright's framing in practice. Henceforth has no dedicated consolidate-to-yourself ritual the user has to remember to perform; every send the user types consumes the available inputs and emits a single change UTXO back to a freshly-derived Type42 address. The wallet consolidates as a side-effect of normal use. ConsolidateUTXO.swift exists for the case where consolidating without spending matters, but the everyday path is the side-effect path — the wallet stays unfragmented because the user keeps spending.


10. What this eliminates, by construction

The properties that hold after the refactor — not by code review, but by the shape of the API surface itself:

  • No clobber from the network. Every network-fetch path is bound to reconcileWalletBalanceFromUTXOs, which can only raise the floor. There is no code path through which confirmedSatoshis can be reduced by a WoC response. The 69 → 32 → 41 flicker class is structurally impossible.
  • No clobber from disk. The launch-time disk load — which may be older than the in-memory state from a parallel iCloud merge — goes through the same floor method. Disk is a baseline, not a ceiling.
  • External spends propagate. A broadcast from another wallet rooted at the same seed appears in /history for a shared address; the observed-spend pass detects it via input scanning and removes the UTXO; the balance updates legitimately via setWalletBalanceFromArray.
  • Cross-device verification state syncs. When iPhone advances a per-address watermark, Mac's next launch reads it and skips re-fetching /history from height 0. The merge semantics handle concurrent advances without a coordinator.
  • Partial progress is preserved. A WoC failure on one address out of five doesn't undo the other four's progress. lastVerifiedAt holds back, but per-address watermarks each advance independently.
  • Rate-limit independence.

Before the refactor, every launch was a fresh round of "ask the chain service for everything." A rate-limit response (HTTP 429 from WoC, ARC-policy-endpoint slowness on a busy moment) reached into the wallet's reconcile path and could degrade what the user saw on screen. The wallet's correctness was effectively a function of WhatsOnChain's recent-minutes performance.

After the refactor, the wallet's source of truth is local: the iCloud-persisted UTXO file, the iCloud-persisted watermark, the in-memory state derived from both. The chain service is consulted only for evidence past the watermark — new TXIDs in /history, new merkle proofs for transactions we haven't yet integrated. If WoC rate-limits us, the verification pass aborts cleanly to .staleNoNetwork with the prior lastVerifiedAt intact, the user sees the staleness banner, and the displayed balance — which has not changed — remains the last value the architecture verified. Rate limits now affect cost and speed; they do not affect correctness.

This is the practical payoff of the four Wright principles compounded together. The wallet uses direct miner submission (Home Node), trusts the chain service only at locally-verifiable layer 2 / layer 4 surfaces (Small Worlds), keeps its sovereignty scoped to "what this device has verified" rather than claiming any chain-wide authority (Sovereign Node), and operates on headers + merkle proofs + watermarks rather than re-querying state every launch (BHT). The four together produce a wallet whose user-facing reliability is decoupled from the chain service's recent moods.


11. What it does not (yet) do

The architecture has a structural property — additive-only state — that opens up two follow-on optimisations:

  1. Server-side /history filtering. WoC's history endpoint doesn't accept a since: parameter; the wallet currently fetches full history and filters client-side against the set of txids it has already integrated. This is correct but wasteful for active wallets. A future BlockchainDataProvider call can use ChainPoint or paginate by height to cut the bytes on the wire.
  2. Halt-on-divergence for cross-source verification. A May 2026 hardening promoted cross-source verification against JungleBus from telemetry mode to the default getHistory path — every history fetch now returns the merged union of WhatsOnChain and JungleBus, with divergence logged. BlockchainDataProvider is an actor so the two fetches run in genuine parallel rather than serialised through the main thread. What remains: halting sync on persistent divergence (today divergence merely surfaces in the merged result; the wallet does not refuse to advance the watermark when the two sources disagree on a transaction's inclusion). Cross-tip cross-verification via getChainTipCrossVerified() is similarly available but not yet wired into the header-sync gate.

Neither gap affects correctness today. The watermark architecture is safety-complete (§7 above), and the merged-union return means any txid either source claims is investigated before the watermark advances. The history-pagination optimisation is a cost tightening; the halt-on-divergence will be a safety tightening for the failure mode where two normally-agreeing sources are concurrently compromised in incompatible ways — a rare condition the wallet currently logs but doesn't block on.


12. Summary

The wallet's state moves in two directions, and the architecture handles each one differently:

  • Up — new evidence (a new UTXO from a payment, a fresh chain tip from a verification pass) raises the floor. Floor methods only.
  • Down — observed evidence of a spend (an input we recognise in a transaction we've fetched) shrinks the local view. Overwrite methods, but only from code that just modified the array itself.

Network responses are evidence inputs; the watermark is the only thing that advances trust; the user-visible balance follows the watermark, not the network's most recent answer. Trust local. Verify the network. Never let silence stand in for a fact.