Skip to content

Electrum do_reorganized calls accumulator.flush() instead of reset(), corrupting subscription status hashes after every reorg #785

@echennells

Description

@echennells

Summary

On a chain regression, protocol_electrum::do_reorganized is meant to clear each subscription's Electrum status-hash midstate so the next notification recomputes from scratch. It calls accumulator.flush(), but flush() finalizes the digest in place (non-idempotent, non-destructive) and does not reset the midstate — reset() is the operation intended to reuse the accumulator. So after do_reorganized the accumulator is left in a non-initial state; it also clears the cursor, so the next confirmed-history scan re-folds the (unchanged) history onto that non-reset state, producing a status hash that no longer equals sha256("::"...). Electrum clients key history re-fetch on status equality, so a wrong-but-stable hash silently diverges a wallet's view of an address from the chain after any reorg.

Code path: src/protocols/electrum/protocol_electrum.cpp:192-203:

  void protocol_electrum::do_reorganized(node::header_t) NOEXCEPT
  {
      ...
      for (auto& [key, sub]: address_subscriptions_)
      {
          sub.accumulator.flush();   // line 199
          sub.status = {};
          sub.cursor = {};
      }
  }

Per its contract (libbitcoin-system, include/bitcoin/system/hash/accumulator.hpp), flush() writes the current state to a digest without resetting it, while reset() returns the accumulator to its initial state for reuse:

/// Flush accumulator state to digest (not idempotent, not destructive).
/// Flush does not reset the accumulator (writes may continue as in pbkd).
  ...
/// Reset accumulator to given or initial state (to reuse after flushing).

do_reorganized wants the initial-state reuse path, so reset() is the correct call here.

Reproduction

Observed on libbitcoin-server master 845b8b3.

  1. blockchain.scripthash.subscribe over a confirmed history; record the returned status hash.
  2. Drive a chase::reorganized (which runs do_reorganized), then a chase::organized that re-scans the unchanged confirmed history.
  3. The re-pushed status hash differs from the original despite identical history — the rescan re-folded onto a flush()ed (non-reset) accumulator.

Impact

After any reorg, surviving address/scripthash subscriptions emit an incorrect Electrum status hash. Because clients trigger history re-fetch only when the status changes and the wrong value is stable, the client silently accepts a divergent view of the address's history — no error is surfaced.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions