Skip to content

Commit 824ae04

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add comprehensive address
Add new module with support for multiple address formats: - Base58Check for traditional P2PKH/P2SH addresses - Bech32/Bech32m for SegWit addresses (P2WPKH/P2WSH/P2TR) - CashAddr for Bitcoin Cash and eCash - Support for various cryptocurrencies (BTC, LTC, BCH, BSV, BTG, etc.) The implementation passes all test vectors and provides a unified interface for encoding/decoding addresses across different networks. Issue: BTC-2650 Co-authored-by: llm-git <[email protected]>
1 parent 7b2d431 commit 824ae04

30 files changed

+3046
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//! Base58Check encoding/decoding for traditional Bitcoin addresses (P2PKH, P2SH).
2+
3+
use super::{AddressCodec, AddressError, Result};
4+
use crate::bitcoin::hashes::Hash;
5+
use crate::bitcoin::{base58, PubkeyHash, Script, ScriptBuf, ScriptHash};
6+
7+
/// Base58Check codec with network-specific version bytes
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
pub struct Base58CheckCodec {
10+
/// Base58Check P2PKH version byte(s)
11+
pub pub_key_hash: u32,
12+
/// Base58Check P2SH version byte(s)
13+
pub script_hash: u32,
14+
}
15+
16+
impl Base58CheckCodec {
17+
/// Create a new Base58Check codec with specified version bytes
18+
pub const fn new(pub_key_hash: u32, script_hash: u32) -> Self {
19+
Self {
20+
pub_key_hash,
21+
script_hash,
22+
}
23+
}
24+
}
25+
26+
/// Encode a hash with version bytes to Base58Check format using bitcoin crate
27+
fn to_base58_check(hash: &[u8], version: u32) -> Result<String> {
28+
let mut data = Vec::new();
29+
30+
// Encode version bytes (1-4 bytes depending on size)
31+
if version <= 0xff {
32+
data.push(version as u8);
33+
} else if version <= 0xffff {
34+
data.extend_from_slice(&(version as u16).to_be_bytes());
35+
} else {
36+
// For Zcash (up to 4 bytes)
37+
let bytes = version.to_be_bytes();
38+
let start = bytes.iter().position(|&b| b != 0).unwrap_or(0);
39+
data.extend_from_slice(&bytes[start..]);
40+
}
41+
42+
data.extend_from_slice(hash);
43+
44+
// Use bitcoin crate's base58 encode_check which adds the checksum
45+
Ok(base58::encode_check(&data))
46+
}
47+
48+
/// Decode a Base58Check address to (hash, version) using bitcoin crate
49+
fn from_base58_check(address: &str) -> Result<(Vec<u8>, u32)> {
50+
// Use bitcoin crate's base58 decode_check which verifies the checksum
51+
let payload =
52+
base58::decode_check(address).map_err(|e| AddressError::Base58Error(e.to_string()))?;
53+
54+
if payload.is_empty() {
55+
return Err(AddressError::Base58Error("Empty payload".to_string()));
56+
}
57+
58+
// Extract version and hash
59+
// Try different version byte lengths
60+
let (version, hash) = if payload.len() >= 21 && (payload[0] == 0x1c || payload[0] == 0x1d) {
61+
// Zcash uses 2-byte versions starting with 0x1c or 0x1d
62+
if payload.len() >= 22 {
63+
let version = u32::from_be_bytes([0, 0, payload[0], payload[1]]);
64+
let hash = payload[2..].to_vec();
65+
(version, hash)
66+
} else {
67+
// Single byte version
68+
let version = payload[0] as u32;
69+
let hash = payload[1..].to_vec();
70+
(version, hash)
71+
}
72+
} else {
73+
// Standard single-byte version
74+
let version = payload[0] as u32;
75+
let hash = payload[1..].to_vec();
76+
(version, hash)
77+
};
78+
79+
Ok((hash, version))
80+
}
81+
82+
impl AddressCodec for Base58CheckCodec {
83+
fn encode(&self, script: &Script) -> Result<String> {
84+
if script.is_p2pkh() {
85+
if script.len() != 25 {
86+
return Err(AddressError::InvalidScript(
87+
"Invalid P2PKH script length".to_string(),
88+
));
89+
}
90+
let hash = &script.as_bytes()[3..23];
91+
to_base58_check(hash, self.pub_key_hash)
92+
} else if script.is_p2sh() {
93+
if script.len() != 23 {
94+
return Err(AddressError::InvalidScript(
95+
"Invalid P2SH script length".to_string(),
96+
));
97+
}
98+
let hash = &script.as_bytes()[2..22];
99+
to_base58_check(hash, self.script_hash)
100+
} else {
101+
Err(AddressError::UnsupportedScriptType(
102+
"Base58Check only supports P2PKH and P2SH".to_string(),
103+
))
104+
}
105+
}
106+
107+
fn decode(&self, address: &str) -> Result<ScriptBuf> {
108+
let (hash, version) = from_base58_check(address)?;
109+
110+
if version == self.pub_key_hash {
111+
let hash_array: [u8; 20] = hash.try_into().map_err(|_| {
112+
AddressError::InvalidAddress("Invalid pubkey hash length".to_string())
113+
})?;
114+
let pubkey_hash = PubkeyHash::from_byte_array(hash_array);
115+
Ok(ScriptBuf::new_p2pkh(&pubkey_hash))
116+
} else if version == self.script_hash {
117+
let hash_array: [u8; 20] = hash.try_into().map_err(|_| {
118+
AddressError::InvalidAddress("Invalid script hash length".to_string())
119+
})?;
120+
let script_hash = ScriptHash::from_byte_array(hash_array);
121+
Ok(ScriptBuf::new_p2sh(&script_hash))
122+
} else {
123+
Err(AddressError::InvalidAddress(format!(
124+
"Version mismatch: expected {} or {}, got {}",
125+
self.pub_key_hash, self.script_hash, version
126+
)))
127+
}
128+
}
129+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! Bech32 and Bech32m encoding/decoding for Bitcoin witness addresses.
2+
//!
3+
//! Implements BIP 173 (Bech32) and BIP 350 (Bech32m) encoding schemes using the bitcoin crate.
4+
//! - Bech32 is used for witness version 0 (P2WPKH, P2WSH)
5+
//! - Bech32m is used for witness version 1+ (P2TR)
6+
7+
use super::{AddressCodec, AddressError, Result};
8+
use crate::bitcoin::{Script, ScriptBuf, WitnessVersion};
9+
10+
/// Bech32/Bech32m codec for witness addresses
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub struct Bech32Codec {
13+
/// Bech32 Human Readable Part (HRP)
14+
pub hrp: &'static str,
15+
}
16+
17+
impl Bech32Codec {
18+
/// Create a new Bech32 codec with the specified HRP
19+
pub const fn new(hrp: &'static str) -> Self {
20+
Self { hrp }
21+
}
22+
}
23+
24+
/// Encode witness program with custom HRP
25+
fn encode_witness_with_custom_hrp(
26+
program: &[u8],
27+
version: WitnessVersion,
28+
hrp_str: &str,
29+
) -> Result<String> {
30+
// Try using the bech32 functionality from bitcoin crate
31+
// The bitcoin crate includes bech32 encoding via its dependencies
32+
use bech32::{self, Hrp};
33+
34+
// Parse the HRP
35+
let hrp = Hrp::parse(hrp_str)
36+
.map_err(|e| AddressError::Bech32Error(format!("Invalid HRP '{}': {}", hrp_str, e)))?;
37+
38+
// Encode based on witness version
39+
let address = if version == WitnessVersion::V0 {
40+
// Use Bech32 for witness version 0
41+
bech32::segwit::encode_v0(hrp, program)
42+
.map_err(|e| AddressError::Bech32Error(format!("Bech32 encoding failed: {}", e)))?
43+
} else {
44+
// Use Bech32m for witness version 1+
45+
bech32::segwit::encode_v1(hrp, program)
46+
.map_err(|e| AddressError::Bech32Error(format!("Bech32m encoding failed: {}", e)))?
47+
};
48+
49+
Ok(address)
50+
}
51+
52+
/// Decode witness program with custom HRP
53+
fn decode_witness_with_custom_hrp(address: &str, expected_hrp: &str) -> Result<Vec<u8>> {
54+
use bech32::{self, Hrp};
55+
56+
// Parse the expected HRP
57+
let expected_hrp_parsed = Hrp::parse(expected_hrp)
58+
.map_err(|e| AddressError::Bech32Error(format!("Invalid HRP '{}': {}", expected_hrp, e)))?;
59+
60+
// Decode the address
61+
let (decoded_hrp, witness_version, witness_program) = bech32::segwit::decode(address)
62+
.map_err(|e| AddressError::Bech32Error(format!("Failed to decode address: {}", e)))?;
63+
64+
// Verify HRP matches
65+
if decoded_hrp != expected_hrp_parsed {
66+
return Err(AddressError::Bech32Error(format!(
67+
"HRP mismatch: expected '{}', got '{}'",
68+
expected_hrp, decoded_hrp
69+
)));
70+
}
71+
72+
// Convert witness version (Fe32) to OP code
73+
// Fe32 can be 0-31, but for segwit, we only care about 0-16
74+
// OP_0 = 0x00, OP_1 = 0x51, OP_2 = 0x52, ... OP_16 = 0x60
75+
let version_byte: u8 = witness_version.to_u8();
76+
let version_opcode = if version_byte == 0 {
77+
0x00 // OP_0
78+
} else if version_byte <= 16 {
79+
0x50 + version_byte // OP_1 through OP_16
80+
} else {
81+
return Err(AddressError::Bech32Error(format!(
82+
"Invalid witness version: {}",
83+
version_byte
84+
)));
85+
};
86+
87+
// Construct the script pubkey: <version> <length> <program>
88+
let mut script = vec![version_opcode, witness_program.len() as u8];
89+
script.extend_from_slice(&witness_program);
90+
Ok(script)
91+
}
92+
93+
impl AddressCodec for Bech32Codec {
94+
fn encode(&self, script: &Script) -> Result<String> {
95+
let (witness_version, program) = if script.is_p2wpkh() {
96+
if script.len() != 22 {
97+
return Err(AddressError::InvalidScript(
98+
"Invalid P2WPKH script length".to_string(),
99+
));
100+
}
101+
(WitnessVersion::V0, &script.as_bytes()[2..22])
102+
} else if script.is_p2wsh() {
103+
if script.len() != 34 {
104+
return Err(AddressError::InvalidScript(
105+
"Invalid P2WSH script length".to_string(),
106+
));
107+
}
108+
(WitnessVersion::V0, &script.as_bytes()[2..34])
109+
} else if script.is_p2tr() {
110+
if script.len() != 34 {
111+
return Err(AddressError::InvalidScript(
112+
"Invalid P2TR script length".to_string(),
113+
));
114+
}
115+
(WitnessVersion::V1, &script.as_bytes()[2..34])
116+
} else {
117+
return Err(AddressError::UnsupportedScriptType(
118+
"Bech32 only supports witness programs (P2WPKH, P2WSH, P2TR)".to_string(),
119+
));
120+
};
121+
122+
// Use custom HRP encoding for all networks
123+
encode_witness_with_custom_hrp(program, witness_version, self.hrp)
124+
}
125+
126+
fn decode(&self, address: &str) -> Result<ScriptBuf> {
127+
// Use custom HRP decoding for all networks
128+
let script_bytes = decode_witness_with_custom_hrp(address, self.hrp)?;
129+
Ok(ScriptBuf::from(script_bytes))
130+
}
131+
}

0 commit comments

Comments
 (0)