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