From d9ebd32003d23331aac5419ab3f63f6af58b68f3 Mon Sep 17 00:00:00 2001 From: "rearden-grok[bot]" <317016512+rearden-grok[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:11:47 -0700 Subject: [PATCH] hex: pin Electrum scripthash display order in the workspace suite sha256: :Hash Display is internal byte order, and the bench crate stays out of the workspace test job. One primitives helper owns the reversed scripthash, and the workspace suite pins it. Co-authored-by: Cursor --- crates/rbitcoin-bench/src/targets.rs | 8 +------- crates/rbitcoin-electrum/src/server.rs | 5 ++--- crates/rbitcoin-primitives/src/hex.rs | 25 +++++++++++++++++++++++++ crates/rbitcoin-primitives/src/lib.rs | 2 +- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/rbitcoin-bench/src/targets.rs b/crates/rbitcoin-bench/src/targets.rs index 162dd394c2..f3b54525bf 100644 --- a/crates/rbitcoin-bench/src/targets.rs +++ b/crates/rbitcoin-bench/src/targets.rs @@ -1,17 +1,11 @@ //! One scripthash or Bitcoin address per line. Embedded corpora in `corpora/`. use bitcoin::address::NetworkUnchecked; -use bitcoin::hashes::{sha256, Hash}; use bitcoin::Address; use std::path::Path; use std::str::FromStr; -/// Electrum / Esplora display scripthash: SHA256(spk) bytes reversed, hex. -pub fn electrum_scripthash_hex(spk: &[u8]) -> String { - rbitcoin_primitives::display_hash_hex(sha256::Hash::hash(spk).to_byte_array()) -} - pub const CORPUS_HOT: &str = include_str!("../corpora/hot.txt"); pub const CORPUS_CASA: &str = include_str!("../corpora/casa.txt"); pub const CORPUS_SPARROW: &str = include_str!("../corpora/sparrow.txt"); @@ -39,7 +33,7 @@ pub fn parse_target_line(line: &str) -> Result, String> { } let addr: Address = Address::from_str(line).map_err(|e| e.to_string())?; let addr = addr.assume_checked(); - Ok(Some(electrum_scripthash_hex( + Ok(Some(rbitcoin_primitives::scripthash_display_hex( addr.script_pubkey().as_bytes(), ))) } diff --git a/crates/rbitcoin-electrum/src/server.rs b/crates/rbitcoin-electrum/src/server.rs index dab8b3f6ee..b4ab586e1f 100644 --- a/crates/rbitcoin-electrum/src/server.rs +++ b/crates/rbitcoin-electrum/src/server.rs @@ -11,7 +11,7 @@ use rbitcoin_net::{BlockingRegion, MempoolHub}; use rbitcoin_primitives::{Fk, Height}; use rbitcoin_query::{ChainView, ChainViewKind, HistoryFilter, Query, ShJoinSlot}; -use rbitcoin_store::{script_hash, StoreError}; +use rbitcoin_store::StoreError; use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; @@ -2617,8 +2617,7 @@ fn scripthash_status_full_slot( /// Helper to compute electrum scripthash hex (reversed) from script bytes. pub fn electrum_scripthash_hex(script: &[u8]) -> String { - let h = script_hash(script); - hash_hex_rev(&h) + rbitcoin_primitives::scripthash_display_hex(script) } #[cfg(test)] diff --git a/crates/rbitcoin-primitives/src/hex.rs b/crates/rbitcoin-primitives/src/hex.rs index 1e94d1d6e7..79d3bc70a1 100644 --- a/crates/rbitcoin-primitives/src/hex.rs +++ b/crates/rbitcoin-primitives/src/hex.rs @@ -8,6 +8,7 @@ //! //! Errors are hex-conservative's own. +use bitcoin_hashes::{sha256, Hash}; use hex_conservative::{DisplayHex, FromHex}; pub use hex_conservative::{HexToArrayError, HexToBytesError}; @@ -40,6 +41,15 @@ pub fn display_hash_hex(mut h: [u8; 32]) -> String { encode(h) } +/// Electrum / Esplora scripthash: SHA256(scriptPubKey) in display order. +/// +/// `sha256::Hash`'s `Display` prints internal byte order. `Txid` display +/// does not. This is the spelling a client sends. +#[inline] +pub fn scripthash_display_hex(script: &[u8]) -> String { + display_hash_hex(sha256::Hash::hash(script).to_byte_array()) +} + /// Parse display-order 32-byte hex → internal byte order. /// /// `0x` / `0X` is stripped here. Any other failure is [`HexToArrayError`]: @@ -55,6 +65,7 @@ pub fn parse_display_hash32(hex: &str) -> Result<[u8; 32], HexToArrayError> { #[cfg(test)] mod tests { use super::*; + use bitcoin_hashes::{sha256, Hash}; #[test] fn roundtrip() { @@ -81,6 +92,20 @@ mod tests { ); } + #[test] + fn p2a_scripthash_display_is_reversed_sha256_not_hash_display() { + // bc1pfeessrawgf. sha256::Hash Display is internal order. + let spk = [0x51, 0x02, 0x4e, 0x73]; + let got = scripthash_display_hex(&spk); + assert_eq!( + got, + "c5d0fb3863474a90cfe5b26801e66cb45fdc046b40e6829788eaf1b8770ffea6" + ); + let digest = sha256::Hash::hash(&spk); + assert_ne!(got, digest.to_string()); + assert_eq!(got, display_hash_hex(digest.to_byte_array())); + } + #[test] fn display_hash_roundtrips_known_internal_bytes() { let mut internal = [0u8; 32]; diff --git a/crates/rbitcoin-primitives/src/lib.rs b/crates/rbitcoin-primitives/src/lib.rs index 92b923a88b..b906fc7425 100644 --- a/crates/rbitcoin-primitives/src/lib.rs +++ b/crates/rbitcoin-primitives/src/lib.rs @@ -17,7 +17,7 @@ pub use compact::{ }; pub use hex::{ decode as hex_decode, display_hash_hex, encode as hex_encode, parse_display_hash32, - HexToArrayError, HexToBytesError, + scripthash_display_hex, HexToArrayError, HexToBytesError, }; pub use loc_simd::{deinterleave_pairs_u8x8, inclusive_u8x8_times_8}; pub use median_time::median_time_past_times;