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.
- blockchain.scripthash.subscribe over a confirmed history; record the returned status hash.
- Drive a chase::reorganized (which runs do_reorganized), then a chase::organized that re-scans the unchanged confirmed history.
- 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.
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:
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:
do_reorganized wants the initial-state reuse path, so reset() is the correct call here.
Reproduction
Observed on libbitcoin-server master 845b8b3.
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.