Skip to main content

bsv_script/
address.rs

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