Skip to main content

bsv/script/
address.rs

1//! Bitcoin address type with Base58Check encoding/decoding.
2//!
3//! Supports mainnet (prefix 0x00) and testnet (prefix 0x6f) addresses.
4//! Translates the Go SDK address.go.
5
6use crate::primitives::public_key::PublicKey;
7use crate::primitives::utils::{base58_check_decode, base58_check_encode};
8use crate::script::error::ScriptError;
9use crate::script::locking_script::LockingScript;
10use crate::script::op::Op;
11use crate::script::script::Script;
12use crate::script::script_chunk::ScriptChunk;
13
14/// Mainnet address prefix byte.
15const MAINNET_PREFIX: u8 = 0x00;
16
17/// Testnet address prefix byte.
18const TESTNET_PREFIX: u8 = 0x6f;
19
20/// A Bitcoin address derived from a public key hash (P2PKH).
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Address {
23    address_string: String,
24    public_key_hash: Vec<u8>,
25}
26
27impl Address {
28    /// Create an address from a 20-byte public key hash.
29    ///
30    /// Uses mainnet prefix (0x00) when `mainnet` is true,
31    /// testnet prefix (0x6f) otherwise.
32    pub fn from_public_key_hash(hash: &[u8; 20], mainnet: bool) -> Self {
33        let prefix_byte = if mainnet {
34            MAINNET_PREFIX
35        } else {
36            TESTNET_PREFIX
37        };
38        let address_string = base58_check_encode(hash, &[prefix_byte]);
39        Address {
40            address_string,
41            public_key_hash: hash.to_vec(),
42        }
43    }
44
45    /// Create an address from a PublicKey.
46    ///
47    /// Computes hash160 of the compressed public key bytes,
48    /// then encodes with the appropriate network prefix.
49    pub fn from_public_key(pubkey: &PublicKey, mainnet: bool) -> Self {
50        let hash_vec = pubkey.to_hash();
51        let mut hash = [0u8; 20];
52        hash.copy_from_slice(&hash_vec);
53        Self::from_public_key_hash(&hash, mainnet)
54    }
55
56    /// Decode an address from a Base58Check string.
57    ///
58    /// Validates that the prefix is 0x00 (mainnet) or 0x6f (testnet)
59    /// and that the hash is exactly 20 bytes.
60    pub fn from_string(s: &str) -> Result<Self, ScriptError> {
61        let (prefix, payload) =
62            base58_check_decode(s, 1).map_err(|e| ScriptError::InvalidAddress(e.to_string()))?;
63
64        if prefix.len() != 1 || (prefix[0] != MAINNET_PREFIX && prefix[0] != TESTNET_PREFIX) {
65            return Err(ScriptError::InvalidAddress(format!(
66                "unknown address prefix: 0x{:02x}",
67                prefix.first().copied().unwrap_or(0)
68            )));
69        }
70
71        if payload.len() != 20 {
72            return Err(ScriptError::InvalidAddress(format!(
73                "expected 20-byte hash, got {} bytes",
74                payload.len()
75            )));
76        }
77
78        Ok(Address {
79            address_string: s.to_string(),
80            public_key_hash: payload,
81        })
82    }
83
84    /// Return the 20-byte public key hash.
85    pub fn to_public_key_hash(&self) -> &[u8] {
86        &self.public_key_hash
87    }
88
89    /// Check whether this is a mainnet address (prefix 0x00).
90    pub fn is_mainnet(&self) -> bool {
91        // Decode the address to check prefix
92        if let Ok((prefix, _)) = base58_check_decode(&self.address_string, 1) {
93            prefix.first().copied() == Some(MAINNET_PREFIX)
94        } else {
95            false
96        }
97    }
98
99    /// Create a P2PKH locking script for this address.
100    ///
101    /// Script: `OP_DUP OP_HASH160 <pubkeyhash> OP_EQUALVERIFY OP_CHECKSIG`
102    pub fn to_locking_script(&self) -> LockingScript {
103        let chunks = vec![
104            ScriptChunk::new_opcode(Op::OpDup),
105            ScriptChunk::new_opcode(Op::OpHash160),
106            ScriptChunk::new_raw(
107                self.public_key_hash.len() as u8,
108                Some(self.public_key_hash.clone()),
109            ),
110            ScriptChunk::new_opcode(Op::OpEqualVerify),
111            ScriptChunk::new_opcode(Op::OpCheckSig),
112        ];
113        LockingScript::from_script(Script::from_chunks(chunks))
114    }
115}
116
117impl std::fmt::Display for Address {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(f, "{}", self.address_string)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    /// Known test vector: private key = 1
127    /// Compressed public key: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
128    /// Public key hash (hash160): 751e76e8199196d454941c45d1b3a323f1433bd6
129    /// Mainnet address: 1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
130    #[test]
131    fn test_known_mainnet_address() {
132        let pubkey_hash =
133            crate::primitives::utils::from_hex("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
134        let mut hash = [0u8; 20];
135        hash.copy_from_slice(&pubkey_hash);
136
137        let addr = Address::from_public_key_hash(&hash, true);
138        assert_eq!(addr.to_string(), "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
139        assert!(addr.is_mainnet());
140    }
141
142    #[test]
143    fn test_known_testnet_address() {
144        let pubkey_hash =
145            crate::primitives::utils::from_hex("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
146        let mut hash = [0u8; 20];
147        hash.copy_from_slice(&pubkey_hash);
148
149        let addr = Address::from_public_key_hash(&hash, false);
150        // Testnet address for same pubkey hash
151        assert!(!addr.is_mainnet());
152        // Testnet addresses start with 'm' or 'n'
153        let s = addr.to_string();
154        assert!(
155            s.starts_with('m') || s.starts_with('n'),
156            "testnet address should start with m or n, got: {}",
157            s
158        );
159    }
160
161    #[test]
162    fn test_roundtrip_from_hash_to_string_from_string() {
163        let pubkey_hash =
164            crate::primitives::utils::from_hex("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
165        let mut hash = [0u8; 20];
166        hash.copy_from_slice(&pubkey_hash);
167
168        let addr = Address::from_public_key_hash(&hash, true);
169        let addr_str = addr.to_string();
170
171        let decoded = Address::from_string(&addr_str).unwrap();
172        assert_eq!(decoded.to_public_key_hash(), &pubkey_hash[..]);
173        assert_eq!(decoded.to_string(), addr_str);
174    }
175
176    #[test]
177    fn test_invalid_address_string() {
178        // Tampered address (checksum mismatch)
179        let result = Address::from_string("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAM1");
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_to_locking_script_p2pkh() {
185        let pubkey_hash =
186            crate::primitives::utils::from_hex("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
187        let mut hash = [0u8; 20];
188        hash.copy_from_slice(&pubkey_hash);
189
190        let addr = Address::from_public_key_hash(&hash, true);
191        let script = addr.to_locking_script();
192
193        // Expected: OP_DUP(76) OP_HASH160(a9) PUSH20(14) <20-byte-hash> OP_EQUALVERIFY(88) OP_CHECKSIG(ac)
194        let binary = script.to_binary();
195        assert_eq!(binary[0], 0x76, "OP_DUP");
196        assert_eq!(binary[1], 0xa9, "OP_HASH160");
197        assert_eq!(binary[2], 0x14, "push 20 bytes");
198        assert_eq!(&binary[3..23], &pubkey_hash[..], "pubkey hash");
199        assert_eq!(binary[23], 0x88, "OP_EQUALVERIFY");
200        assert_eq!(binary[24], 0xac, "OP_CHECKSIG");
201        assert_eq!(binary.len(), 25);
202    }
203
204    #[test]
205    fn test_from_public_key() {
206        // Use private key = 1 to get the known public key
207        use crate::primitives::private_key::PrivateKey;
208
209        let pk = PrivateKey::from_bytes(&{
210            let mut buf = [0u8; 32];
211            buf[31] = 1;
212            buf
213        })
214        .unwrap();
215        let pubkey = pk.to_public_key();
216        let addr = Address::from_public_key(&pubkey, true);
217        assert_eq!(addr.to_string(), "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
218    }
219
220    #[test]
221    fn test_display_impl() {
222        let pubkey_hash =
223            crate::primitives::utils::from_hex("751e76e8199196d454941c45d1b3a323f1433bd6").unwrap();
224        let mut hash = [0u8; 20];
225        hash.copy_from_slice(&pubkey_hash);
226
227        let addr = Address::from_public_key_hash(&hash, true);
228        let display_str = format!("{}", addr);
229        assert_eq!(display_str, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH");
230    }
231}