Skip to content

electrum: hash the protocol's txid:height: status preimage - #1031

Merged
reardencode merged 1 commit into
reardencode:masterfrom
bkeroack:electrum/protocol-status
Oct 16, 2026
Merged

reardencode merged 1 commit into
reardencode:masterfrom
bkeroack:electrum/protocol-status

Conversation

@bkeroack

@bkeroack bkeroack commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1028.

Problem

For confirmed rows, scripthash_status hashed txid:height:blockhash:. The Electrum protocol defines the status as SHA-256 of tx_hash:height: per get_history row. Electrum desktop recomputes that from get_history (synchronizer.py history_status). On a mismatch it does not store the history, and after the network timeout it raises SynchronizerFailure and disconnects. So any address with a confirmed transaction never synced.

Change

  • scripthash_status hashes tx_hash:height: for every row and needs no Query. cached_confirming_hash and the header lookup are gone.
  • A same-height replace that keeps every (txid, height) now keeps the status, so the reorg restatus pushes nothing for that script. The chain_tip stamp on responses still changes, and that remains the A-B-A signal (COMPAT.md Chain view). A reorg that moves a row's height, or drops a row, still changes the status and pushes, as before.
  • COMPAT.md, TESTING.md, and changelog.d/electrum-protocol-status.md are updated to match.

Tests

  • electrum_server_version_history_balance (row-order pin) now builds the expected status from the protocol preimage over get_history. It fails on master: server 332d64a8… vs protocol 7b3cee0c….
  • electrum_and_esplora_asof_hides_later_spend: after the same-height replace and the A-B-A restore, no status push arrives, and a fresh subscribe answers the original status. On master a push arrives, so the test fails. The unspend step still pushes a new status.
  • Unit: a one-row status equals sha256("<txid>:1:").

Local: fmt, clippy -D warnings, deny, ast-grep.sh, cargo test --workspace, and ci-os-smoke.sh all pass.

Clients still learn about a same-height replace: blockchain.headers.subscribe already pushes the replaced tip's header on every TipNotify, and Electrum re-verifies merkle proofs on a header reorg.

🤖 Generated with Claude Code

Confirmed rows hashed txid:height:blockhash:, so the status no longer
matched the one the Electrum protocol defines. Electrum desktop
recomputes the status from get_history (synchronizer.py
history_status). On a mismatch it does not store the history, and after
the network timeout it raises SynchronizerFailure and disconnects. Every
address with a confirmed transaction never synced.

The status is now SHA-256 of tx_hash:height: for each get_history row.
A same-height replace that keeps every (txid, height) keeps the status
and pushes nothing; chain_tip on responses still marks it. The asof
journey pins that instead of a changed status, and the row-order pin
recomputes the protocol preimage.

Fixes reardencode#1028

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@reardencode

Copy link
Copy Markdown
Owner

Ahh. I was slightly afraid this extra hash element was gonna bite me. Seems like a good idea, but didn't think clients were depending on it in this way.

Thank you!

@rearden-grok rearden-grok Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Issue #1028 is right. Confirmed scripthash status hashed txid:height:blockhash:, and Electrum desktop recomputes SHA-256 of tx_hash:height: from get_history (synchronizer.history_status). Those never match, so it refuses to store the history and disconnects. This PR puts the status back on that protocol preimage.

The new digest is lowercase hex of the raw SHA-256. txid_hex is the same display-order string get_history puts in tx_hash. Heights, including mempool 0 and -1, are the same i64s on those rows. Fee is not hashed. An empty history stays "" internally and null on the wire. Subscribe and restatus still hash the full history plus the mempool tail, not a from_height / to_height window.

A same-height replace that keeps every (txid, height) no longer changes the status, and take_new_status already skips an unchanged string. blockchain.headers.subscribe still pushes the new tip header, which is what Electrum desktop uses to fork its header chain and re-verify merkle proofs. chain_tip on the next get_history still restamps. The journey now asserts no status push across that replace and the A-B-A restore, and disconnecting the spend still pushes. The socket under test is subscribed to the scripthash only, so a header notification cannot make that drain non-empty.

COMPAT, the TESTING cell, and changelog.d/electrum-protocol-status.md match. Historical CHANGELOG.md text is left alone, which is the right place for an already-released note.

Issue counts by severity

  • bugs: 0
  • suggestions: 0
  • nits: 0

@reardencode
reardencode merged commit 3f2a0bf into reardencode:master Oct 16, 2026
18 checks passed
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.

electrum: scripthash status includes the block hash, so Electrum desktop never syncs a confirmed address

2 participants