1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/// Bitcoin address handling.
///
/// Supports P2PKH address generation from public key hashes,
/// address validation, and mainnet/testnet discrimination.
/// Uses Base58Check encoding with SHA-256d checksums.
use std::fmt;
use bsv_primitives::hash::{hash160, sha256d};
use crate::ScriptError;
/// Mainnet P2PKH address version byte.
const MAINNET_P2PKH: u8 = 0x00;
/// Testnet P2PKH address version byte.
const TESTNET_P2PKH: u8 = 0x6f;
/// Bitcoin network type for address prefix selection.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Network {
/// Bitcoin mainnet (address prefix 0x00, starts with '1').
Mainnet,
/// Bitcoin testnet (address prefix 0x6f, starts with 'm' or 'n').
Testnet,
}
/// A Bitcoin P2PKH address.
///
/// Contains the 20-byte public key hash and the network it belongs to.
/// Can be serialized to/from the Base58Check string format.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Address {
/// The human-readable Base58Check address string.
pub address_string: String,
/// The 20-byte RIPEMD-160(SHA-256(pubkey)) hash.
pub public_key_hash: [u8; 20],
/// The network this address belongs to.
pub network: Network,
}
impl Address {
/// Parse a Base58Check-encoded address string.
///
/// Decodes the string, validates the checksum, and detects the network
/// from the version byte (0x00 = mainnet, 0x6f = testnet).
///
/// # Arguments
/// * `addr` - The Base58Check address string.
///
/// # Returns
/// An `Address` or an error if the string is invalid.
pub fn from_string(addr: &str) -> Result<Self, ScriptError> {
let decoded = bs58::decode(addr)
.into_vec()
.map_err(|_| ScriptError::InvalidAddress(format!("bad char for '{}'", addr)))?;
if decoded.len() != 25 {
return Err(ScriptError::InvalidAddressLength(addr.to_string()));
}
// Verify checksum: last 4 bytes should equal sha256d of first 21 bytes.
let checksum = sha256d(&decoded[..21]);
if decoded[21..25] != checksum[..4] {
return Err(ScriptError::EncodingChecksumFailed);
}
let network = match decoded[0] {
MAINNET_P2PKH => Network::Mainnet,
TESTNET_P2PKH => Network::Testnet,
_ => return Err(ScriptError::UnsupportedAddress(addr.to_string())),
};
let mut pkh = [0u8; 20];
pkh.copy_from_slice(&decoded[1..21]);
Ok(Address {
address_string: addr.to_string(),
public_key_hash: pkh,
network,
})
}
/// Create an address from a 20-byte public key hash.
///
/// # Arguments
/// * `hash` - The 20-byte hash160 of the public key.
/// * `network` - The target network (Mainnet or Testnet).
///
/// # Returns
/// A new `Address` with the encoded Base58Check string.
pub fn from_public_key_hash(hash: &[u8; 20], network: Network) -> Self {
let version = match network {
Network::Mainnet => MAINNET_P2PKH,
Network::Testnet => TESTNET_P2PKH,
};
let mut payload = Vec::with_capacity(25);
payload.push(version);
payload.extend_from_slice(hash);
let checksum = sha256d(&payload);
payload.extend_from_slice(&checksum[..4]);
let address_string = bs58::encode(&payload).into_string();
Address {
address_string,
public_key_hash: *hash,
network,
}
}
/// Create a mainnet address from a hex-encoded public key string.
///
/// Computes hash160 of the decoded public key bytes and produces
/// a mainnet address.
///
/// # Arguments
/// * `pub_key_hex` - Hex-encoded public key (compressed or uncompressed).
///
/// # Returns
/// A mainnet `Address`, or an error if the hex is invalid.
pub fn from_public_key_string(pub_key_hex: &str, mainnet: bool) -> Result<Self, ScriptError> {
let pub_key_bytes = hex::decode(pub_key_hex)?;
let h = hash160(&pub_key_bytes);
let network = if mainnet {
Network::Mainnet
} else {
Network::Testnet
};
Ok(Self::from_public_key_hash(&h, network))
}
}
impl fmt::Display for Address {
/// Display the address as its Base58Check string.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.address_string)
}
}
#[cfg(test)]
mod tests {
//! Tests for Bitcoin address parsing, generation, and validation.
//!
//! Covers Address::from_string for mainnet/testnet addresses, checksum
//! validation, network detection, Address::from_public_key_hash for both
//! networks, Address::from_public_key_string roundtrips, Display output,
//! and error cases for short/unsupported addresses. Test vectors are
//! derived from the Go SDK reference implementation.
use super::*;
/// The public key hash shared across several test vectors.
const TEST_PUBLIC_KEY_HASH: &str = "00ac6144c4db7b5790f343cf0477a65fb8a02eb7";
// -----------------------------------------------------------------------
// from_string (mainnet)
// -----------------------------------------------------------------------
/// Parse a known mainnet address and verify the public key hash and network.
#[test]
fn test_from_string_mainnet() {
let address_str = "1E7ucTTWRTahCyViPhxSMor2pj4VGQdFMr";
let addr = Address::from_string(address_str).expect("should parse mainnet");
assert_eq!(addr.address_string, address_str);
assert_eq!(
hex::encode(addr.public_key_hash),
"8fe80c75c9560e8b56ed64ea3c26e18d2c52211b"
);
assert_eq!(addr.network, Network::Mainnet);
}
// -----------------------------------------------------------------------
// from_string (testnet)
// -----------------------------------------------------------------------
/// Parse a known testnet address and verify the public key hash and network.
#[test]
fn test_from_string_testnet() {
let address_str = "mtdruWYVEV1wz5yL7GvpBj4MgifCB7yhPd";
let addr = Address::from_string(address_str).expect("should parse testnet");
assert_eq!(addr.address_string, address_str);
assert_eq!(
hex::encode(addr.public_key_hash),
"8fe80c75c9560e8b56ed64ea3c26e18d2c52211b"
);
assert_eq!(addr.network, Network::Testnet);
}
/// Mainnet and testnet addresses for the same PKH should decode to the same hash.
#[test]
fn test_from_string_same_pkh_different_networks() {
let mainnet_addr = Address::from_string("1E7ucTTWRTahCyViPhxSMor2pj4VGQdFMr")
.expect("mainnet should parse");
let testnet_addr = Address::from_string("mtdruWYVEV1wz5yL7GvpBj4MgifCB7yhPd")
.expect("testnet should parse");
assert_eq!(mainnet_addr.public_key_hash, testnet_addr.public_key_hash);
}
// -----------------------------------------------------------------------
// from_string - error cases
// -----------------------------------------------------------------------
/// Verify that a short/invalid address returns an error.
#[test]
fn test_from_string_short_address() {
let result = Address::from_string("ADD8E55");
assert!(result.is_err());
}
/// Verify that an address with an unsupported version byte returns an error.
#[test]
fn test_from_string_unsupported_version() {
let result = Address::from_string("27BvY7rFguYQvEL872Y7Fo77Y3EBApC2EK");
assert!(result.is_err());
}
// -----------------------------------------------------------------------
// from_public_key_string
// -----------------------------------------------------------------------
/// Create a mainnet address from a compressed public key hex string.
#[test]
fn test_from_public_key_string_mainnet() {
let addr = Address::from_public_key_string(
"026cf33373a9f3f6c676b75b543180703df225f7f8edbffedc417718a8ad4e89ce",
true,
)
.expect("should create address");
assert_eq!(hex::encode(addr.public_key_hash), TEST_PUBLIC_KEY_HASH);
assert_eq!(addr.address_string, "114ZWApV4EEU8frr7zygqQcB1V2BodGZuS");
assert_eq!(addr.network, Network::Mainnet);
}
/// Create a testnet address from the same compressed public key hex string.
#[test]
fn test_from_public_key_string_testnet() {
let addr = Address::from_public_key_string(
"026cf33373a9f3f6c676b75b543180703df225f7f8edbffedc417718a8ad4e89ce",
false,
)
.expect("should create address");
assert_eq!(hex::encode(addr.public_key_hash), TEST_PUBLIC_KEY_HASH);
assert_eq!(addr.address_string, "mfaWoDuTsFfiunLTqZx4fKpVsUctiDV9jk");
assert_eq!(addr.network, Network::Testnet);
}
/// Verify that an invalid public key hex returns an error.
#[test]
fn test_from_public_key_string_invalid() {
let result = Address::from_public_key_string("invalid_pubkey", true);
assert!(result.is_err());
}
// -----------------------------------------------------------------------
// from_public_key_hash
// -----------------------------------------------------------------------
/// Create a mainnet address from a raw 20-byte public key hash.
#[test]
fn test_from_public_key_hash_mainnet() {
let hash_bytes = hex::decode(TEST_PUBLIC_KEY_HASH).expect("valid hex");
let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_bytes);
let addr = Address::from_public_key_hash(&hash, Network::Mainnet);
assert_eq!(addr.public_key_hash, hash);
assert_eq!(addr.address_string, "114ZWApV4EEU8frr7zygqQcB1V2BodGZuS");
assert_eq!(addr.network, Network::Mainnet);
}
/// Create a testnet address from a raw 20-byte public key hash.
#[test]
fn test_from_public_key_hash_testnet() {
let hash_bytes = hex::decode(TEST_PUBLIC_KEY_HASH).expect("valid hex");
let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_bytes);
let addr = Address::from_public_key_hash(&hash, Network::Testnet);
assert_eq!(addr.public_key_hash, hash);
assert_eq!(addr.address_string, "mfaWoDuTsFfiunLTqZx4fKpVsUctiDV9jk");
assert_eq!(addr.network, Network::Testnet);
}
// -----------------------------------------------------------------------
// String roundtrip
// -----------------------------------------------------------------------
/// Verify that from_string -> to_string produces the original address.
#[test]
fn test_address_to_string_roundtrip_mainnet() {
let address_str = "1E7ucTTWRTahCyViPhxSMor2pj4VGQdFMr";
let addr = Address::from_string(address_str).expect("should parse");
assert_eq!(format!("{}", addr), address_str);
}
/// Verify testnet address string roundtrip.
#[test]
fn test_address_to_string_roundtrip_testnet() {
let address_str = "mtdruWYVEV1wz5yL7GvpBj4MgifCB7yhPd";
let addr = Address::from_string(address_str).expect("should parse");
assert_eq!(format!("{}", addr), address_str);
}
/// Verify that from_public_key_hash -> from_string roundtrip is consistent.
#[test]
fn test_public_key_hash_to_address_to_string_roundtrip() {
let hash_bytes = hex::decode(TEST_PUBLIC_KEY_HASH).expect("valid hex");
let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_bytes);
let addr = Address::from_public_key_hash(&hash, Network::Mainnet);
let parsed = Address::from_string(&addr.address_string).expect("should parse back");
assert_eq!(addr.public_key_hash, parsed.public_key_hash);
assert_eq!(addr.address_string, parsed.address_string);
assert_eq!(addr.network, parsed.network);
}
}