From a1cd6fa3dabe0559e6546e1036be4fc090c5bba2 Mon Sep 17 00:00:00 2001 From: Matt Faltyn Date: Tue, 14 Jul 2026 20:28:05 +0200 Subject: [PATCH 1/2] fix p2pk hash-prefix lookup --- crates/brk_query/src/impl/addr/hash_prefix.rs | 104 ++++++++++++------ 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/crates/brk_query/src/impl/addr/hash_prefix.rs b/crates/brk_query/src/impl/addr/hash_prefix.rs index 886faac80..79c4045f1 100644 --- a/crates/brk_query/src/impl/addr/hash_prefix.rs +++ b/crates/brk_query/src/impl/addr/hash_prefix.rs @@ -16,50 +16,57 @@ impl Query { } let prefix = AddrHashPrefix::parse(prefix)?; - let store = self - .indexer() - .stores - .addr_type_to_addr_hash_to_addr_index - .get(addr_type) - .data()?; - let safe_type_index = self.safe_lengths().to_type_index(addr_type); let addr_readers = self.indexer().vecs.addrs.addr_readers(); let mut addresses = Vec::new(); let max_hash = AddrHash::new(u64::MAX); - if let Some(upper) = prefix.upper { - for (_, type_index) in store.range(prefix.lower..upper) { - if type_index >= safe_type_index { - continue; + for &search_type in matching_addr_types(&addr_type) { + let store = self + .indexer() + .stores + .addr_type_to_addr_hash_to_addr_index + .get(search_type) + .data()?; + let safe_type_index = self.safe_lengths().to_type_index(search_type); + + if let Some(upper) = prefix.upper { + for (_, type_index) in store.range(prefix.lower..upper) { + if type_index >= safe_type_index { + continue; + } + + let script = addr_readers.script_pubkey(search_type, type_index); + addresses.push(Addr::try_from((&script, search_type))?); + + if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { + break; + } + } + } else { + for (_, type_index) in store.range(prefix.lower..max_hash) { + if type_index >= safe_type_index { + continue; + } + + let script = addr_readers.script_pubkey(search_type, type_index); + addresses.push(Addr::try_from((&script, search_type))?); + + if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { + break; + } } - let script = addr_readers.script_pubkey(addr_type, type_index); - addresses.push(Addr::try_from((&script, addr_type))?); - - if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { - break; - } - } - } else { - for (_, type_index) in store.range(prefix.lower..max_hash) { - if type_index >= safe_type_index { - continue; - } - - let script = addr_readers.script_pubkey(addr_type, type_index); - addresses.push(Addr::try_from((&script, addr_type))?); - - if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { - break; + if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT + && let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned()) + && type_index < safe_type_index + { + let script = addr_readers.script_pubkey(search_type, type_index); + addresses.push(Addr::try_from((&script, search_type))?); } } - if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT - && let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned()) - && type_index < safe_type_index - { - let script = addr_readers.script_pubkey(addr_type, type_index); - addresses.push(Addr::try_from((&script, addr_type))?); + if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { + break; } } @@ -75,6 +82,13 @@ impl Query { } } +fn matching_addr_types(addr_type: &OutputType) -> &[OutputType] { + match addr_type { + OutputType::P2PK65 | OutputType::P2PK33 => &[OutputType::P2PK65, OutputType::P2PK33], + _ => std::slice::from_ref(addr_type), + } +} + struct AddrHashPrefix { text: String, lower: AddrHash, @@ -113,3 +127,23 @@ impl AddrHashPrefix { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p2pk_wire_type_searches_both_internal_types() { + let wire_type: OutputType = serde_json::from_str(r#""p2pk""#).unwrap(); + + assert_eq!(wire_type, OutputType::P2PK65); + assert_eq!( + matching_addr_types(&wire_type), + &[OutputType::P2PK65, OutputType::P2PK33] + ); + assert_eq!( + matching_addr_types(&OutputType::P2PKH), + &[OutputType::P2PKH] + ); + } +} From be14c8a160201be095f7336e5d65618909bdfe9c Mon Sep 17 00:00:00 2001 From: Matt Faltyn Date: Mon, 20 Jul 2026 10:45:29 +0200 Subject: [PATCH 2/2] separate native and normalized output types --- .../src/generators/javascript/client.rs | 9 +- .../src/generators/python/client.rs | 10 +- crates/brk_bindgen/src/generators/rust/api.rs | 17 --- .../brk_bindgen/src/generators/rust/client.rs | 23 ++- crates/brk_client/src/lib.rs | 24 ++- crates/brk_client/tests/hash_prefix.rs | 17 +++ crates/brk_query/src/impl/addr/hash_prefix.rs | 104 +++++-------- crates/brk_types/src/addr_stats.rs | 2 +- crates/brk_types/src/output_type.rs | 141 ++++++++++++++++-- crates/brk_types/src/txout.rs | 62 +++++++- .../brk-client/docs/interfaces/AddrStats.md | 2 +- .../docs/type-aliases/OutputType.md | 2 +- modules/brk-client/index.js | 13 +- modules/brk-client/tests/hash_prefix.js | 26 +++- packages/brk_client/brk_client/__init__.py | 14 +- .../addresses/test_address_info.py | 6 +- packages/brk_client/tests/test_hash_prefix.py | 17 ++- 17 files changed, 336 insertions(+), 153 deletions(-) diff --git a/crates/brk_bindgen/src/generators/javascript/client.rs b/crates/brk_bindgen/src/generators/javascript/client.rs index c96f25357..51bf2049f 100644 --- a/crates/brk_bindgen/src/generators/javascript/client.rs +++ b/crates/brk_bindgen/src/generators/javascript/client.rs @@ -825,12 +825,13 @@ function _validateHashPrefixNibbles(nibbles) { function _addressPayloadLengths(addrType) { switch (addrType) { case "p2a": return [2]; - case "p2pk": return [33, 65]; + case "p2pk33": return [33]; + case "p2pk65": return [65]; case "p2pkh": case "p2sh": - case "v0_p2wpkh": return [20]; - case "v0_p2wsh": - case "v1_p2tr": return [32]; + case "p2wpkh": return [20]; + case "p2wsh": + case "p2tr": return [32]; default: throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`); } diff --git a/crates/brk_bindgen/src/generators/python/client.rs b/crates/brk_bindgen/src/generators/python/client.rs index c169c248d..eec2bd514 100644 --- a/crates/brk_bindgen/src/generators/python/client.rs +++ b/crates/brk_bindgen/src/generators/python/client.rs @@ -242,11 +242,13 @@ def _validate_hash_prefix_nibbles(nibbles: int) -> None: def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]: if addr_type == "p2a": return (2,) - if addr_type == "p2pk": - return (33, 65) - if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"): + if addr_type == "p2pk33": + return (33,) + if addr_type == "p2pk65": + return (65,) + if addr_type in ("p2pkh", "p2sh", "p2wpkh"): return (20,) - if addr_type in ("v0_p2wsh", "v1_p2tr"): + if addr_type in ("p2wsh", "p2tr"): return (32,) raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}") diff --git a/crates/brk_bindgen/src/generators/rust/api.rs b/crates/brk_bindgen/src/generators/rust/api.rs index 247fdd931..11281bd1a 100644 --- a/crates/brk_bindgen/src/generators/rust/api.rs +++ b/crates/brk_bindgen/src/generators/rust/api.rs @@ -148,23 +148,6 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) { "get_text" }; - if endpoint.path == "/api/address/hash-prefix/{addr_type}/{prefix}" { - writeln!( - output, - " let addr_type = address_payload_type_path(addr_type)?;" - ) - .unwrap(); - writeln!( - output, - " self.base.{}(&format!(\"{}\"{}))", - fetch_method, path, index_arg - ) - .unwrap(); - writeln!(output, " }}").unwrap(); - writeln!(output).unwrap(); - return; - } - if endpoint.query_params.is_empty() { writeln!( output, diff --git a/crates/brk_bindgen/src/generators/rust/client.rs b/crates/brk_bindgen/src/generators/rust/client.rs index 45ba155f1..e7f10ced8 100644 --- a/crates/brk_bindgen/src/generators/rust/client.rs +++ b/crates/brk_bindgen/src/generators/rust/client.rs @@ -83,7 +83,6 @@ fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> R return Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }}); }}, }}; - let addr_type = address_payload_type_path(addr_type)?; if !expected.contains(&payload.len()) {{ let joined = expected @@ -97,18 +96,16 @@ fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> R Ok(()) }} -fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {{ - match addr_type {{ - OutputType::P2A => Ok("p2a"), - OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"), - OutputType::P2PKH => Ok("p2pkh"), - OutputType::P2SH => Ok("p2sh"), - OutputType::P2WPKH => Ok("v0_p2wpkh"), - OutputType::P2WSH => Ok("v0_p2wsh"), - OutputType::P2TR => Ok("v1_p2tr"), - OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{ - Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }}) - }}, +#[cfg(test)] +mod address_payload_tests {{ + use super::*; + + #[test] + fn p2pk_payload_lengths_are_distinct() {{ + assert!(validate_address_payload_for_type(OutputType::P2PK33, &[0; 33]).is_ok()); + assert!(validate_address_payload_for_type(OutputType::P2PK65, &[0; 65]).is_ok()); + assert!(validate_address_payload_for_type(OutputType::P2PK33, &[0; 65]).is_err()); + assert!(validate_address_payload_for_type(OutputType::P2PK65, &[0; 33]).is_err()); }} }} diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index f8b461d38..38a2ad965 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -72,7 +72,6 @@ fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> R return Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") }); }, }; - let addr_type = address_payload_type_path(addr_type)?; if !expected.contains(&payload.len()) { let joined = expected @@ -86,18 +85,16 @@ fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> R Ok(()) } -fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> { - match addr_type { - OutputType::P2A => Ok("p2a"), - OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"), - OutputType::P2PKH => Ok("p2pkh"), - OutputType::P2SH => Ok("p2sh"), - OutputType::P2WPKH => Ok("v0_p2wpkh"), - OutputType::P2WSH => Ok("v0_p2wsh"), - OutputType::P2TR => Ok("v1_p2tr"), - OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => { - Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") }) - }, +#[cfg(test)] +mod address_payload_tests { + use super::*; + + #[test] + fn p2pk_payload_lengths_are_distinct() { + assert!(validate_address_payload_for_type(OutputType::P2PK33, &[0; 33]).is_ok()); + assert!(validate_address_payload_for_type(OutputType::P2PK65, &[0; 65]).is_ok()); + assert!(validate_address_payload_for_type(OutputType::P2PK33, &[0; 65]).is_err()); + assert!(validate_address_payload_for_type(OutputType::P2PK65, &[0; 33]).is_err()); } } @@ -9992,7 +9989,6 @@ impl BrkClient { /// /// Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}` pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result { - let addr_type = address_payload_type_path(addr_type)?; self.base.get_json(&format!("/api/address/hash-prefix/{addr_type}/{prefix}")) } diff --git a/crates/brk_client/tests/hash_prefix.rs b/crates/brk_client/tests/hash_prefix.rs index 7ca3b96d0..4240e18cb 100644 --- a/crates/brk_client/tests/hash_prefix.rs +++ b/crates/brk_client/tests/hash_prefix.rs @@ -42,3 +42,20 @@ fn address_hash_prefix_uses_brk_address_parser() { address_payload_hash_prefix(&decoded.payload, 8).unwrap() ); } + +#[test] +fn p2pk_payload_types_are_distinct() { + let p2pk33 = decode_address_payload( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ) + .unwrap(); + assert_eq!(p2pk33.addr_type, OutputType::P2PK33); + assert_eq!(p2pk33.payload.len(), 33); + + let p2pk65 = decode_address_payload( + "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f", + ) + .unwrap(); + assert_eq!(p2pk65.addr_type, OutputType::P2PK65); + assert_eq!(p2pk65.payload.len(), 65); +} diff --git a/crates/brk_query/src/impl/addr/hash_prefix.rs b/crates/brk_query/src/impl/addr/hash_prefix.rs index 79c4045f1..886faac80 100644 --- a/crates/brk_query/src/impl/addr/hash_prefix.rs +++ b/crates/brk_query/src/impl/addr/hash_prefix.rs @@ -16,57 +16,50 @@ impl Query { } let prefix = AddrHashPrefix::parse(prefix)?; + let store = self + .indexer() + .stores + .addr_type_to_addr_hash_to_addr_index + .get(addr_type) + .data()?; + let safe_type_index = self.safe_lengths().to_type_index(addr_type); let addr_readers = self.indexer().vecs.addrs.addr_readers(); let mut addresses = Vec::new(); let max_hash = AddrHash::new(u64::MAX); - for &search_type in matching_addr_types(&addr_type) { - let store = self - .indexer() - .stores - .addr_type_to_addr_hash_to_addr_index - .get(search_type) - .data()?; - let safe_type_index = self.safe_lengths().to_type_index(search_type); - - if let Some(upper) = prefix.upper { - for (_, type_index) in store.range(prefix.lower..upper) { - if type_index >= safe_type_index { - continue; - } - - let script = addr_readers.script_pubkey(search_type, type_index); - addresses.push(Addr::try_from((&script, search_type))?); - - if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { - break; - } - } - } else { - for (_, type_index) in store.range(prefix.lower..max_hash) { - if type_index >= safe_type_index { - continue; - } - - let script = addr_readers.script_pubkey(search_type, type_index); - addresses.push(Addr::try_from((&script, search_type))?); - - if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { - break; - } + if let Some(upper) = prefix.upper { + for (_, type_index) in store.range(prefix.lower..upper) { + if type_index >= safe_type_index { + continue; } - if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT - && let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned()) - && type_index < safe_type_index - { - let script = addr_readers.script_pubkey(search_type, type_index); - addresses.push(Addr::try_from((&script, search_type))?); + let script = addr_readers.script_pubkey(addr_type, type_index); + addresses.push(Addr::try_from((&script, addr_type))?); + + if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { + break; + } + } + } else { + for (_, type_index) in store.range(prefix.lower..max_hash) { + if type_index >= safe_type_index { + continue; + } + + let script = addr_readers.script_pubkey(addr_type, type_index); + addresses.push(Addr::try_from((&script, addr_type))?); + + if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { + break; } } - if addresses.len() > ADDR_HASH_PREFIX_MATCH_LIMIT { - break; + if addresses.len() <= ADDR_HASH_PREFIX_MATCH_LIMIT + && let Some(type_index) = store.get(&max_hash)?.map(|cow| cow.into_owned()) + && type_index < safe_type_index + { + let script = addr_readers.script_pubkey(addr_type, type_index); + addresses.push(Addr::try_from((&script, addr_type))?); } } @@ -82,13 +75,6 @@ impl Query { } } -fn matching_addr_types(addr_type: &OutputType) -> &[OutputType] { - match addr_type { - OutputType::P2PK65 | OutputType::P2PK33 => &[OutputType::P2PK65, OutputType::P2PK33], - _ => std::slice::from_ref(addr_type), - } -} - struct AddrHashPrefix { text: String, lower: AddrHash, @@ -127,23 +113,3 @@ impl AddrHashPrefix { )) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn p2pk_wire_type_searches_both_internal_types() { - let wire_type: OutputType = serde_json::from_str(r#""p2pk""#).unwrap(); - - assert_eq!(wire_type, OutputType::P2PK65); - assert_eq!( - matching_addr_types(&wire_type), - &[OutputType::P2PK65, OutputType::P2PK33] - ); - assert_eq!( - matching_addr_types(&OutputType::P2PKH), - &[OutputType::P2PKH] - ); - } -} diff --git a/crates/brk_types/src/addr_stats.rs b/crates/brk_types/src/addr_stats.rs index 93c4e761e..b623763d2 100644 --- a/crates/brk_types/src/addr_stats.rs +++ b/crates/brk_types/src/addr_stats.rs @@ -12,7 +12,7 @@ pub struct AddrStats { #[serde(rename = "address")] pub addr: Addr, - /// Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) + /// BRK address type (p2pk33, p2pk65, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, etc.) pub addr_type: OutputType, /// Statistics for confirmed transactions on the blockchain diff --git a/crates/brk_types/src/output_type.rs b/crates/brk_types/src/output_type.rs index eb46c153f..c2f18e346 100644 --- a/crates/brk_types/src/output_type.rs +++ b/crates/brk_types/src/output_type.rs @@ -1,8 +1,3 @@ -#![allow( - unreachable_patterns, - reason = "P2PK65 and P2PK33 both serialize as 'p2pk'" -)] - use bitcoin::{AddressType, ScriptBuf, opcodes::all::OP_PUSHBYTES_2}; use brk_error::Error; use schemars::JsonSchema; @@ -31,13 +26,27 @@ use crate::AddrBytes; #[repr(u8)] /// Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) pub enum OutputType { - #[serde(rename = "p2pk")] - #[strum(serialize = "p2pk")] P2PK65, - #[serde(rename = "p2pk")] - #[strum(serialize = "p2pk")] P2PK33, P2PKH, + P2MS, + P2SH, + OpReturn, + P2WPKH, + P2WSH, + P2TR, + P2A, + Empty, + Unknown, +} + +#[derive(Debug, Clone, Copy, Display, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Hash)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +/// Output type names used by Esplora and mempool.space. +pub enum OutputTypeNormalized { + P2PK, + P2PKH, #[serde(rename = "multisig")] #[strum(serialize = "multisig")] P2MS, @@ -117,6 +126,22 @@ impl OutputType { !self.is_spendable() } + pub const fn normalized(self) -> OutputTypeNormalized { + match self { + Self::P2PK65 | Self::P2PK33 => OutputTypeNormalized::P2PK, + Self::P2PKH => OutputTypeNormalized::P2PKH, + Self::P2MS => OutputTypeNormalized::P2MS, + Self::P2SH => OutputTypeNormalized::P2SH, + Self::OpReturn => OutputTypeNormalized::OpReturn, + Self::P2WPKH => OutputTypeNormalized::P2WPKH, + Self::P2WSH => OutputTypeNormalized::P2WSH, + Self::P2TR => OutputTypeNormalized::P2TR, + Self::P2A => OutputTypeNormalized::P2A, + Self::Empty => OutputTypeNormalized::Empty, + Self::Unknown => OutputTypeNormalized::Unknown, + } + } + /// Whether the address type's public key is revealed at funding time /// (vs. only at spending time). For P2PK33/P2PK65 the pubkey is directly /// in the locking script; for P2TR the tweaked output key is in the @@ -278,3 +303,101 @@ impl Pco for OutputType { } impl TransparentPco for OutputType {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn native_and_normalized_names_are_distinct() { + let cases = [ + ( + OutputType::P2PK65, + "p2pk65", + OutputTypeNormalized::P2PK, + "p2pk", + ), + ( + OutputType::P2PK33, + "p2pk33", + OutputTypeNormalized::P2PK, + "p2pk", + ), + ( + OutputType::P2PKH, + "p2pkh", + OutputTypeNormalized::P2PKH, + "p2pkh", + ), + ( + OutputType::P2MS, + "p2ms", + OutputTypeNormalized::P2MS, + "multisig", + ), + (OutputType::P2SH, "p2sh", OutputTypeNormalized::P2SH, "p2sh"), + ( + OutputType::OpReturn, + "opreturn", + OutputTypeNormalized::OpReturn, + "op_return", + ), + ( + OutputType::P2WPKH, + "p2wpkh", + OutputTypeNormalized::P2WPKH, + "v0_p2wpkh", + ), + ( + OutputType::P2WSH, + "p2wsh", + OutputTypeNormalized::P2WSH, + "v0_p2wsh", + ), + ( + OutputType::P2TR, + "p2tr", + OutputTypeNormalized::P2TR, + "v1_p2tr", + ), + (OutputType::P2A, "p2a", OutputTypeNormalized::P2A, "p2a"), + ( + OutputType::Empty, + "empty", + OutputTypeNormalized::Empty, + "empty", + ), + ( + OutputType::Unknown, + "unknown", + OutputTypeNormalized::Unknown, + "unknown", + ), + ]; + + for (native, native_name, normalized, normalized_name) in cases { + assert_eq!(native.to_string(), native_name); + assert_eq!(native.normalized(), normalized); + assert_eq!( + serde_json::to_string(&native).unwrap(), + format!(r#""{native_name}""#) + ); + assert_eq!( + serde_json::from_str::(&format!(r#""{native_name}""#)).unwrap(), + native + ); + assert_eq!(normalized.to_string(), normalized_name); + assert_eq!( + serde_json::to_string(&normalized).unwrap(), + format!(r#""{normalized_name}""#) + ); + assert_eq!( + serde_json::from_str::(&format!(r#""{normalized_name}""#)) + .unwrap(), + normalized + ); + } + + assert!(serde_json::from_str::(r#""p2pk""#).is_err()); + } +} diff --git a/crates/brk_types/src/txout.rs b/crates/brk_types/src/txout.rs index 469246640..c6ed563e2 100644 --- a/crates/brk_types/src/txout.rs +++ b/crates/brk_types/src/txout.rs @@ -26,10 +26,10 @@ pub struct TxOut { )] script_pubkey_asm: (), - /// Script type (p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, op_return, etc.) + /// Esplora/mempool.space script type #[allow(dead_code)] #[serde(skip, rename = "scriptpubkey_type")] - #[schemars(with = "OutputType", example = &"v0_p2wpkh")] + #[schemars(with = "crate::OutputTypeNormalized", example = &"v0_p2wpkh")] script_pubkey_type: (), /// Bitcoin address (if applicable, None for OP_RETURN) @@ -108,7 +108,7 @@ impl Serialize for TxOut { let mut state = serializer.serialize_struct("TxOut", field_count)?; state.serialize_field("scriptpubkey", &self.script_pubkey.to_hex_string())?; state.serialize_field("scriptpubkey_asm", &self.script_pubkey_asm())?; - state.serialize_field("scriptpubkey_type", &output_type)?; + state.serialize_field("scriptpubkey_type", &output_type.normalized())?; if let Some(addr) = &addr { state.serialize_field("scriptpubkey_address", addr)?; } @@ -116,3 +116,59 @@ impl Serialize for TxOut { state.end() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn p2pk_script(pubkey: &[u8]) -> ScriptBuf { + let mut script = Vec::with_capacity(pubkey.len() + 2); + script.push(pubkey.len() as u8); + script.extend_from_slice(pubkey); + script.push(0xac); + ScriptBuf::from_bytes(script) + } + + fn script_type(script: ScriptBuf) -> (OutputType, String) { + let txout = TxOut::from((script, Sats::new(0))); + let output_type = txout.type_(); + let value = serde_json::to_value(txout).unwrap(); + let script_type = value["scriptpubkey_type"].as_str().unwrap().to_owned(); + (output_type, script_type) + } + + #[test] + fn script_type_uses_normalized_names() { + let p2pk33 = [ + 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, + 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, + 0x5b, 0x16, 0xf8, 0x17, 0x98, + ]; + let p2pk65 = [ + 0x04, 0x67, 0x8a, 0xfd, 0xb0, 0xfe, 0x55, 0x48, 0x27, 0x19, 0x67, 0xf1, 0xa6, 0x71, + 0x30, 0xb7, 0x10, 0x5c, 0xd6, 0xa8, 0x28, 0xe0, 0x39, 0x09, 0xa6, 0x79, 0x62, 0xe0, + 0xea, 0x1f, 0x61, 0xde, 0xb6, 0x49, 0xf6, 0xbc, 0x3f, 0x4c, 0xef, 0x38, 0xc4, 0xf3, + 0x55, 0x04, 0xe5, 0x1e, 0xc1, 0x12, 0xde, 0x5c, 0x38, 0x4d, 0xf7, 0xba, 0x0b, 0x8d, + 0x57, 0x8a, 0x4c, 0x70, 0x2b, 0x6b, 0xf1, 0x1d, 0x5f, + ]; + + assert_eq!( + script_type(p2pk_script(&p2pk33)), + (OutputType::P2PK33, "p2pk".to_string()) + ); + assert_eq!( + script_type(p2pk_script(&p2pk65)), + (OutputType::P2PK65, "p2pk".to_string()) + ); + let mut p2wpkh = vec![0; 22]; + p2wpkh[1] = 0x14; + assert_eq!( + script_type(ScriptBuf::from_bytes(p2wpkh)), + (OutputType::P2WPKH, "v0_p2wpkh".to_string()) + ); + assert_eq!( + script_type(ScriptBuf::from_bytes(vec![0x6a])), + (OutputType::OpReturn, "op_return".to_string()) + ); + } +} diff --git a/modules/brk-client/docs/interfaces/AddrStats.md b/modules/brk-client/docs/interfaces/AddrStats.md index c05f5b6cf..d99151b9f 100644 --- a/modules/brk-client/docs/interfaces/AddrStats.md +++ b/modules/brk-client/docs/interfaces/AddrStats.md @@ -26,7 +26,7 @@ Bitcoin address string Defined in: [Developer/brk/modules/brk-client/index.js:67](https://github.com/bitcoinresearchkit/brk/blob/7a718293c0ddbae305c8352474c81c0e99fe1200/modules/brk-client/index.js#L67) -Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) +BRK address type (p2pk33, p2pk65, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, etc.) *** diff --git a/modules/brk-client/docs/type-aliases/OutputType.md b/modules/brk-client/docs/type-aliases/OutputType.md index 642abe258..a3b6849a6 100644 --- a/modules/brk-client/docs/type-aliases/OutputType.md +++ b/modules/brk-client/docs/type-aliases/OutputType.md @@ -6,7 +6,7 @@ # Type Alias: OutputType -> **OutputType** = `"p2pk"` \| `"p2pk"` \| `"p2pkh"` \| `"multisig"` \| `"p2sh"` \| `"op_return"` \| `"v0_p2wpkh"` \| `"v0_p2wsh"` \| `"v1_p2tr"` \| `"p2a"` \| `"empty"` \| `"unknown"` +> **OutputType** = `"p2pk65"` \| `"p2pk33"` \| `"p2pkh"` \| `"p2ms"` \| `"p2sh"` \| `"opreturn"` \| `"p2wpkh"` \| `"p2wsh"` \| `"p2tr"` \| `"p2a"` \| `"empty"` \| `"unknown"` Defined in: [Developer/brk/modules/brk-client/index.js:839](https://github.com/bitcoinresearchkit/brk/blob/7a718293c0ddbae305c8352474c81c0e99fe1200/modules/brk-client/index.js#L839) diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 5a78938a6..088eb06ba 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -64,7 +64,7 @@ * * @typedef {Object} AddrStats * @property {Addr} address - Bitcoin address string - * @property {OutputType} addrType - Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) + * @property {OutputType} addrType - BRK address type (p2pk33, p2pk65, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, etc.) * @property {AddrChainStats} chainStats - Statistics for confirmed transactions on the blockchain * @property {AddrMempoolStats} mempoolStats - Statistics for unconfirmed transactions in the mempool */ @@ -837,7 +837,7 @@ ancestors and no descendants (matches mempool.space). /** * Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) * - * @typedef {("p2pk"|"p2pk"|"p2pkh"|"multisig"|"p2sh"|"op_return"|"v0_p2wpkh"|"v0_p2wsh"|"v1_p2tr"|"p2a"|"empty"|"unknown")} OutputType + * @typedef {("p2pk65"|"p2pk33"|"p2pkh"|"p2ms"|"p2sh"|"opreturn"|"p2wpkh"|"p2wsh"|"p2tr"|"p2a"|"empty"|"unknown")} OutputType */ /** @typedef {TypeIndex} P2AAddrIndex */ /** @typedef {U8x2} P2ABytes */ @@ -2270,12 +2270,13 @@ function _validateHashPrefixNibbles(nibbles) { function _addressPayloadLengths(addrType) { switch (addrType) { case "p2a": return [2]; - case "p2pk": return [33, 65]; + case "p2pk33": return [33]; + case "p2pk65": return [65]; case "p2pkh": case "p2sh": - case "v0_p2wpkh": return [20]; - case "v0_p2wsh": - case "v1_p2tr": return [32]; + case "p2wpkh": return [20]; + case "p2wsh": + case "p2tr": return [32]; default: throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`); } diff --git a/modules/brk-client/tests/hash_prefix.js b/modules/brk-client/tests/hash_prefix.js index c0778871c..4510c2c43 100644 --- a/modules/brk-client/tests/hash_prefix.js +++ b/modules/brk-client/tests/hash_prefix.js @@ -24,11 +24,35 @@ assert.deepEqual( client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.from({ length: 20 }, (_, i) => i), 8), { addrType: "p2pkh", prefix: "c3327ecb", truncated: false, addresses: [] }, ); +for (const [addrType, length] of [["p2pk33", 33], ["p2pk65", 65]]) { + const payload = Uint8Array.from({ length }, (_, i) => i); + assert.deepEqual( + client.getAddressPayloadHashPrefixMatches(addrType, payload, 8), + { + addrType, + prefix: addressPayloadHashPrefix(payload, 8), + truncated: false, + addresses: [], + }, + ); +} assert.throws( () => client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.of(1, 2), 8), /p2pkh address payload length 20 bytes/, ); assert.throws( - () => client.getAddressPayloadHashPrefixMatches("op_return", Uint8Array.of(1, 2), 8), + () => client.getAddressPayloadHashPrefixMatches("p2pk33", new Uint8Array(65), 8), + /p2pk33 address payload length 33 bytes/, +); +assert.throws( + () => client.getAddressPayloadHashPrefixMatches("p2pk65", new Uint8Array(33), 8), + /p2pk65 address payload length 65 bytes/, +); +assert.throws( + () => client.getAddressPayloadHashPrefixMatches("p2pk", new Uint8Array(33), 8), + /Unsupported address type/, +); +assert.throws( + () => client.getAddressPayloadHashPrefixMatches("opreturn", Uint8Array.of(1, 2), 8), /Unsupported address type/, ); diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 1d6e0bd67..89964deff 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -28,7 +28,7 @@ Sats = int # Index within its type (e.g., 0 for first P2WPKH address) TypeIndex = int # Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) -OutputType = Literal["p2pk", "p2pk", "p2pkh", "multisig", "p2sh", "op_return", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr", "p2a", "empty", "unknown"] +OutputType = Literal["p2pk65", "p2pk33", "p2pkh", "p2ms", "p2sh", "opreturn", "p2wpkh", "p2wsh", "p2tr", "p2a", "empty", "unknown"] # Unified index for any address type (funded or empty) AnyAddrIndex = TypeIndex # Unsigned basis points stored as u16. @@ -339,7 +339,7 @@ class AddrStats(TypedDict): Attributes: address: Bitcoin address string - addr_type: Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) + addr_type: BRK address type (p2pk33, p2pk65, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, etc.) chain_stats: Statistics for confirmed transactions on the blockchain mempool_stats: Statistics for unconfirmed transactions in the mempool """ @@ -2009,11 +2009,13 @@ def _validate_hash_prefix_nibbles(nibbles: int) -> None: def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]: if addr_type == "p2a": return (2,) - if addr_type == "p2pk": - return (33, 65) - if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"): + if addr_type == "p2pk33": + return (33,) + if addr_type == "p2pk65": + return (65,) + if addr_type in ("p2pkh", "p2sh", "p2wpkh"): return (20,) - if addr_type in ("v0_p2wsh", "v1_p2tr"): + if addr_type in ("p2wsh", "p2tr"): return (32,) raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}") diff --git a/packages/brk_client/tests/mempool_compat/addresses/test_address_info.py b/packages/brk_client/tests/mempool_compat/addresses/test_address_info.py index 48bc6f249..9c6f1b723 100644 --- a/packages/brk_client/tests/mempool_compat/addresses/test_address_info.py +++ b/packages/brk_client/tests/mempool_compat/addresses/test_address_info.py @@ -8,8 +8,8 @@ from _lib import assert_same_structure, show KNOWN_ADDR_TYPES = { - "p2pk", "p2pkh", "p2sh", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr", - "multisig", "op_return", "p2a", "empty", "unknown", + "p2pk33", "p2pk65", "p2pkh", "p2sh", "p2wpkh", "p2wsh", "p2tr", + "p2ms", "opreturn", "p2a", "empty", "unknown", } # Static fixtures: stable addresses with known shapes. @@ -124,7 +124,7 @@ def test_address_invalid(brk): def test_address_pubkey_as_address(brk): """Brk-only: hex-encoded pubkey is accepted as a P2PK address.""" b = brk.get_address(SATOSHI_GENESIS_PUBKEY) - assert b["addr_type"] == "p2pk", f"expected p2pk, got {b['addr_type']!r}" + assert b["addr_type"] == "p2pk65", f"expected p2pk65, got {b['addr_type']!r}" assert b["chain_stats"]["funded_txo_count"] >= 1, ( f"genesis pubkey must have at least one funded output, got " f"{b['chain_stats']['funded_txo_count']}" diff --git a/packages/brk_client/tests/test_hash_prefix.py b/packages/brk_client/tests/test_hash_prefix.py index bbe2b8c14..9aa38cd8c 100644 --- a/packages/brk_client/tests/test_hash_prefix.py +++ b/packages/brk_client/tests/test_hash_prefix.py @@ -42,7 +42,22 @@ def test_address_payload_hash_prefix_match_validation(): "addresses": [], } + for addr_type, length in (("p2pk33", 33), ("p2pk65", 65)): + payload = bytes(range(length)) + assert client.get_address_payload_hash_prefix_matches(addr_type, payload, 8) == { + "addr_type": addr_type, + "prefix": address_payload_hash_prefix(payload, 8), + "truncated": False, + "addresses": [], + } + with pytest.raises(ValueError, match="p2pkh address payload length 20 bytes"): client.get_address_payload_hash_prefix_matches("p2pkh", b"\x01\x02", 8) + with pytest.raises(ValueError, match="p2pk33 address payload length 33 bytes"): + client.get_address_payload_hash_prefix_matches("p2pk33", bytes(65), 8) + with pytest.raises(ValueError, match="p2pk65 address payload length 65 bytes"): + client.get_address_payload_hash_prefix_matches("p2pk65", bytes(33), 8) with pytest.raises(ValueError, match="Unsupported address type"): - client.get_address_payload_hash_prefix_matches("op_return", b"\x01\x02", 8) + client.get_address_payload_hash_prefix_matches("p2pk", bytes(33), 8) + with pytest.raises(ValueError, match="Unsupported address type"): + client.get_address_payload_hash_prefix_matches("opreturn", b"\x01\x02", 8)