Skip to main content

dpp/address_funds/
platform_address.rs

1use crate::address_funds::AddressWitness;
2use crate::address_funds::AddressWitnessVerificationOperations;
3use crate::prelude::AddressNonce;
4use crate::ProtocolError;
5use bech32::{Bech32m, Hrp};
6use bincode::{Decode, Encode};
7use dashcore::address::Payload;
8use dashcore::blockdata::script::ScriptBuf;
9use dashcore::hashes::{sha256d, Hash};
10use dashcore::key::Secp256k1;
11use dashcore::secp256k1::ecdsa::RecoverableSignature;
12use dashcore::secp256k1::Message;
13use dashcore::signer::CompactSignature;
14use dashcore::{Address, Network, PrivateKey, PubkeyHash, PublicKey, ScriptHash};
15use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
16#[cfg(feature = "serde-conversion")]
17use serde::{Deserialize, Serialize};
18use std::convert::TryFrom;
19use std::str::FromStr;
20
21/// The size of the address hash (20 bytes for both P2PKH and P2SH)
22pub const ADDRESS_HASH_SIZE: usize = 20;
23
24#[derive(
25    Debug,
26    PartialEq,
27    Eq,
28    Clone,
29    Copy,
30    Hash,
31    Ord,
32    PartialOrd,
33    Encode,
34    Decode,
35    PlatformSerialize,
36    PlatformDeserialize,
37)]
38#[platform_serialize(unversioned)]
39pub enum PlatformAddress {
40    /// Pay to pubkey hash
41    /// - bech32m encoding type byte: 0xb0
42    /// - storage key type byte: 0x00
43    P2pkh([u8; 20]),
44    /// Pay to script hash
45    /// - bech32m encoding type byte: 0x80
46    /// - storage key type byte: 0x01
47    P2sh([u8; 20]),
48}
49
50// Custom serde impls so JSON / `platform_value` output is the canonical 21-byte
51// address representation (hex string in human-readable formats, raw bytes in
52// binary formats) — matching the wasm wrapper's serde and what consumers expect.
53// The `Encode` / `Decode` derives above are the consensus binary format and are
54// untouched.
55#[cfg(feature = "serde-conversion")]
56impl Serialize for PlatformAddress {
57    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58    where
59        S: serde::Serializer,
60    {
61        let bytes = self.to_bytes();
62        if serializer.is_human_readable() {
63            serializer.serialize_str(&hex::encode(&bytes))
64        } else {
65            serializer.serialize_bytes(&bytes)
66        }
67    }
68}
69
70#[cfg(feature = "serde-conversion")]
71impl<'de> Deserialize<'de> for PlatformAddress {
72    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73    where
74        D: serde::Deserializer<'de>,
75    {
76        use serde::de::{self, Visitor};
77        use std::fmt;
78
79        /// Maximum on-the-wire byte length for a `PlatformAddress`: 1 type byte + 20 hash bytes.
80        const PLATFORM_ADDRESS_BYTE_LEN: usize = 21;
81
82        struct PlatformAddressVisitor;
83
84        impl<'de> Visitor<'de> for PlatformAddressVisitor {
85            type Value = PlatformAddress;
86
87            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
88                formatter.write_str("PlatformAddress as 21 bytes or hex string")
89            }
90
91            fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
92                let bytes =
93                    hex::decode(value).map_err(|err| E::custom(format!("invalid hex: {}", err)))?;
94                if bytes.len() != PLATFORM_ADDRESS_BYTE_LEN {
95                    return Err(E::invalid_length(bytes.len(), &self));
96                }
97                PlatformAddress::from_bytes(&bytes).map_err(|err| E::custom(err.to_string()))
98            }
99
100            fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
101                self.visit_str(&value)
102            }
103
104            fn visit_bytes<E: de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
105                if value.len() != PLATFORM_ADDRESS_BYTE_LEN {
106                    return Err(E::invalid_length(value.len(), &self));
107                }
108                PlatformAddress::from_bytes(value).map_err(|err| E::custom(err.to_string()))
109            }
110
111            fn visit_byte_buf<E: de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
112                self.visit_bytes(&value)
113            }
114
115            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
116            where
117                A: de::SeqAccess<'de>,
118            {
119                // Cap at PLATFORM_ADDRESS_BYTE_LEN + 1 so we can detect over-long input
120                // without allocating arbitrary memory from a malicious peer.
121                let mut bytes = Vec::with_capacity(PLATFORM_ADDRESS_BYTE_LEN);
122                while let Some(byte) = seq.next_element::<u8>()? {
123                    if bytes.len() >= PLATFORM_ADDRESS_BYTE_LEN {
124                        return Err(de::Error::invalid_length(
125                            bytes.len() + 1,
126                            &"at most 21 bytes",
127                        ));
128                    }
129                    bytes.push(byte);
130                }
131                if bytes.len() != PLATFORM_ADDRESS_BYTE_LEN {
132                    return Err(de::Error::invalid_length(bytes.len(), &self));
133                }
134                PlatformAddress::from_bytes(&bytes)
135                    .map_err(|err| de::Error::custom(err.to_string()))
136            }
137        }
138
139        // Dispatch on the format's self-description: human-readable formats (JSON, TOML)
140        // get the hex-string path; binary formats get raw bytes. This avoids the
141        // `deserialize_any` pitfall on non-self-describing transports.
142        if deserializer.is_human_readable() {
143            deserializer.deserialize_str(PlatformAddressVisitor)
144        } else {
145            deserializer.deserialize_bytes(PlatformAddressVisitor)
146        }
147    }
148}
149
150impl TryFrom<Address> for PlatformAddress {
151    type Error = ProtocolError;
152
153    fn try_from(address: Address) -> Result<Self, Self::Error> {
154        match address.payload() {
155            Payload::PubkeyHash(hash) => Ok(PlatformAddress::P2pkh(*hash.as_ref())),
156            Payload::ScriptHash(hash) => Ok(PlatformAddress::P2sh(*hash.as_ref())),
157            _ => Err(ProtocolError::DecodingError(
158                "unsupported address type for PlatformAddress: only P2PKH and P2SH are supported"
159                    .to_string(),
160            )),
161        }
162    }
163}
164
165impl From<&PrivateKey> for PlatformAddress {
166    /// Derives a P2PKH Platform address from a private key.
167    ///
168    /// The address is derived as: P2PKH(Hash160(compressed_public_key))
169    /// where Hash160 = RIPEMD160(SHA256(x)), which is the standard Bitcoin P2PKH derivation.
170    fn from(private_key: &PrivateKey) -> Self {
171        let secp = Secp256k1::new();
172        let pubkey_hash = private_key.public_key(&secp).pubkey_hash();
173        PlatformAddress::P2pkh(*pubkey_hash.as_byte_array())
174    }
175}
176
177impl Default for PlatformAddress {
178    fn default() -> Self {
179        PlatformAddress::P2pkh([0u8; 20])
180    }
181}
182
183/// Human-readable part for Platform addresses on mainnet (DIP-0018)
184pub const PLATFORM_HRP_MAINNET: &str = "dash";
185/// Human-readable part for Platform addresses on testnet/devnet/regtest (DIP-0018)
186pub const PLATFORM_HRP_TESTNET: &str = "tdash";
187
188/// Validates an already-lowercased HRP and returns whether it is mainnet.
189///
190/// `true` = mainnet (`dash`), `false` = non-mainnet (`tdash`).
191/// Returns an error for any other value.
192pub(crate) fn classify_platform_hrp(hrp: &str) -> Result<bool, ProtocolError> {
193    match hrp {
194        PLATFORM_HRP_MAINNET => Ok(true),
195        PLATFORM_HRP_TESTNET => Ok(false),
196        other => Err(ProtocolError::DecodingError(format!(
197            "not a platform address: HRP '{other}' is neither \
198             '{PLATFORM_HRP_MAINNET}' nor '{PLATFORM_HRP_TESTNET}'"
199        ))),
200    }
201}
202
203impl PlatformAddress {
204    /// Type byte for P2PKH addresses in bech32m encoding (user-facing)
205    pub const P2PKH_TYPE: u8 = 0xb0;
206    /// Type byte for P2SH addresses in bech32m encoding (user-facing)
207    pub const P2SH_TYPE: u8 = 0x80;
208
209    /// Returns the appropriate HRP (Human-Readable Part) for the given network.
210    ///
211    /// Per DIP-0018:
212    /// - Mainnet: "dash"
213    /// - Testnet/Devnet/Regtest: "tdash"
214    pub fn hrp_for_network(network: Network) -> &'static str {
215        match network {
216            Network::Mainnet => PLATFORM_HRP_MAINNET,
217            Network::Testnet | Network::Devnet | Network::Regtest => PLATFORM_HRP_TESTNET,
218        }
219    }
220
221    /// Encodes the PlatformAddress as a bech32m string for the specified network.
222    ///
223    /// The encoding follows DIP-0018:
224    /// - Format: `<HRP>1<data-part>`
225    /// - Data: type_byte (0xb0 for P2PKH, 0x80 for P2SH) || 20-byte hash
226    /// - Checksum: bech32m (BIP-350)
227    ///
228    /// NOTE: This uses bech32m type bytes (0xb0/0x80) for user-facing addresses,
229    /// NOT the storage type bytes (0x00/0x01) used in GroveDB keys.
230    ///
231    /// # Example
232    /// ```ignore
233    /// let address = PlatformAddress::P2pkh([0xf7, 0xda, ...]);
234    /// let encoded = address.to_bech32m_string(Network::Mainnet);
235    /// // Returns something like "dash1k..."
236    /// ```
237    pub fn to_bech32m_string(&self, network: Network) -> String {
238        let hrp_str = Self::hrp_for_network(network);
239        let hrp = Hrp::parse(hrp_str).expect("HRP is valid");
240
241        // Build the 21-byte payload: type_byte || hash
242        // Using bech32m type bytes (0xb0/0x80), NOT storage type bytes (0x00/0x01)
243        let mut payload = Vec::with_capacity(1 + ADDRESS_HASH_SIZE);
244        match self {
245            PlatformAddress::P2pkh(hash) => {
246                payload.push(Self::P2PKH_TYPE);
247                payload.extend_from_slice(hash);
248            }
249            PlatformAddress::P2sh(hash) => {
250                payload.push(Self::P2SH_TYPE);
251                payload.extend_from_slice(hash);
252            }
253        }
254
255        // Verified that this can not error
256        bech32::encode::<Bech32m>(hrp, &payload).expect("encoding should succeed")
257    }
258
259    /// Decodes a bech32m-encoded Platform address string per DIP-0018.
260    ///
261    /// Accepts both `dash` (mainnet) and `tdash` (non-mainnet) HRPs.
262    /// The address is network-agnostic; callers that need a network guard should
263    /// use [`is_mainnet_bech32m`](Self::is_mainnet_bech32m) before decoding.
264    ///
265    /// # Returns
266    /// - `Ok(PlatformAddress)` - The decoded address
267    /// - `Err(ProtocolError)` - If the string is malformed or its HRP is not a
268    ///   recognized platform HRP
269    pub fn from_bech32m_string(s: &str) -> Result<Self, ProtocolError> {
270        let (hrp, data) =
271            bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
272
273        classify_platform_hrp(&hrp.as_str().to_ascii_lowercase())?;
274
275        // Validate payload length: 1 type byte + 20 hash bytes = 21 bytes
276        if data.len() != 1 + ADDRESS_HASH_SIZE {
277            return Err(ProtocolError::DecodingError(format!(
278                "invalid Platform address length: expected {} bytes, got {}",
279                1 + ADDRESS_HASH_SIZE,
280                data.len()
281            )));
282        }
283
284        // Parse using bech32m type bytes (0xb0/0x80), NOT storage type bytes
285        let address_type = data[0];
286        let hash: [u8; 20] = data[1..21]
287            .try_into()
288            .map_err(|_| ProtocolError::DecodingError("invalid hash length".to_string()))?;
289
290        let address = match address_type {
291            Self::P2PKH_TYPE => Ok(PlatformAddress::P2pkh(hash)),
292            Self::P2SH_TYPE => Ok(PlatformAddress::P2sh(hash)),
293            _ => Err(ProtocolError::DecodingError(format!(
294                "invalid address type: 0x{:02x}",
295                address_type
296            ))),
297        }?;
298
299        Ok(address)
300    }
301
302    /// Classifies a bech32m platform-address string as mainnet or non-mainnet.
303    ///
304    /// Fully decodes `s` (validating checksum and data part) then classifies
305    /// the HRP: `dash` means mainnet, `tdash` means non-mainnet (Testnet /
306    /// Devnet / Regtest — these are indistinguishable by HRP alone per DIP-0018).
307    ///
308    /// # Returns
309    /// - `Ok(true)` - mainnet (`dash` HRP)
310    /// - `Ok(false)` - non-mainnet (`tdash` HRP: Testnet/Devnet/Regtest)
311    /// - `Err(ProtocolError)` - malformed address or non-platform HRP
312    pub fn is_mainnet_bech32m(s: &str) -> Result<bool, ProtocolError> {
313        let (hrp, _) =
314            bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{e}")))?;
315        classify_platform_hrp(&hrp.to_lowercase())
316    }
317
318    /// Converts the PlatformAddress to a dashcore Address with the specified network.
319    pub fn to_address_with_network(&self, network: Network) -> Address {
320        match self {
321            PlatformAddress::P2pkh(hash) => Address::new(
322                network,
323                Payload::PubkeyHash(PubkeyHash::from_byte_array(*hash)),
324            ),
325            PlatformAddress::P2sh(hash) => Address::new(
326                network,
327                Payload::ScriptHash(ScriptHash::from_byte_array(*hash)),
328            ),
329        }
330    }
331
332    /// Converts the PlatformAddress to bytes for storage keys.
333    /// Format: [variant_index (1 byte)] + [hash (20 bytes)]
334    ///
335    /// Uses bincode serialization which produces: 0x00 for P2pkh, 0x01 for P2sh.
336    /// These bytes are used as keys in GroveDB.
337    pub fn to_bytes(&self) -> Vec<u8> {
338        bincode::encode_to_vec(self, bincode::config::standard())
339            .expect("PlatformAddress serialization cannot fail")
340    }
341
342    /// Gets a base64 string of the PlatformAddress concatenated with the nonce.
343    /// This creates a unique identifier for address-based state transition inputs.
344    pub fn base64_string_with_nonce(&self, nonce: AddressNonce) -> String {
345        use base64::engine::general_purpose::STANDARD;
346        use base64::Engine;
347
348        let mut bytes = self.to_bytes();
349        bytes.extend_from_slice(&nonce.to_be_bytes());
350
351        STANDARD.encode(bytes)
352    }
353
354    /// Creates a PlatformAddress from storage bytes.
355    /// Format: [variant_index (1 byte)] + [hash (20 bytes)]
356    ///
357    /// Uses bincode deserialization which expects: 0x00 for P2pkh, 0x01 for P2sh.
358    pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
359        let (address, _): (Self, usize) =
360            bincode::decode_from_slice(bytes, bincode::config::standard()).map_err(|e| {
361                ProtocolError::DecodingError(format!("cannot decode PlatformAddress: {}", e))
362            })?;
363        Ok(address)
364    }
365
366    /// Returns the hash portion of the address (20 bytes)
367    pub fn hash(&self) -> &[u8; 20] {
368        match self {
369            PlatformAddress::P2pkh(hash) => hash,
370            PlatformAddress::P2sh(hash) => hash,
371        }
372    }
373
374    /// Returns true if this is a P2PKH address
375    pub fn is_p2pkh(&self) -> bool {
376        matches!(self, PlatformAddress::P2pkh(_))
377    }
378
379    /// Returns true if this is a P2SH address
380    pub fn is_p2sh(&self) -> bool {
381        matches!(self, PlatformAddress::P2sh(_))
382    }
383
384    /// Verifies that the provided witness matches this address and that signatures are valid.
385    ///
386    /// For P2PKH addresses:
387    /// - The witness must be `AddressWitness::P2pkh`
388    /// - The public key must hash to this address
389    /// - The signature must be valid for the signable bytes
390    ///
391    /// For P2SH addresses:
392    /// - The witness must be `AddressWitness::P2sh`
393    /// - The redeem script must hash to this address
394    /// - For multisig scripts: M valid signatures must be provided for the signable bytes
395    ///
396    /// # Arguments
397    /// * `witness` - The witness containing signature(s) and either a public key (P2PKH) or redeem script (P2SH)
398    /// * `signable_bytes` - The data that was signed (will be double-SHA256 hashed internally)
399    ///
400    /// # Returns
401    /// * `Ok(AddressWitnessVerificationOperations)` - Operations performed if verification succeeds
402    /// * `Err(ProtocolError)` if verification fails
403    pub fn verify_bytes_against_witness(
404        &self,
405        witness: &AddressWitness,
406        signable_bytes: &[u8],
407    ) -> Result<AddressWitnessVerificationOperations, ProtocolError> {
408        match (self, witness) {
409            (PlatformAddress::P2pkh(pubkey_hash), AddressWitness::P2pkh { signature }) => {
410                // Use verify_hash_signature which:
411                // 1. Computes double_sha256(signable_bytes)
412                // 2. Recovers the public key from the signature
413                // 3. Verifies Hash160(recovered_pubkey) matches pubkey_hash
414                //
415                // This saves 33 bytes per witness (no need to include pubkey)
416                // at a ~4% CPU cost increase (recovery vs verify).
417                let data_hash = dashcore::signer::double_sha(signable_bytes);
418                dashcore::signer::verify_hash_signature(
419                    &data_hash,
420                    signature.as_slice(),
421                    pubkey_hash,
422                )
423                .map_err(|e| {
424                    ProtocolError::AddressWitnessError(format!(
425                        "P2PKH signature verification failed: {}",
426                        e
427                    ))
428                })?;
429
430                Ok(AddressWitnessVerificationOperations::for_p2pkh(
431                    signable_bytes.len(),
432                ))
433            }
434            (
435                PlatformAddress::P2sh(script_hash),
436                AddressWitness::P2sh {
437                    signatures,
438                    redeem_script,
439                },
440            ) => {
441                // First verify the redeem script hashes to the address
442                let script = ScriptBuf::from_bytes(redeem_script.to_vec());
443                let computed_hash = script.script_hash();
444                if computed_hash.as_byte_array() != script_hash {
445                    return Err(ProtocolError::AddressWitnessError(format!(
446                        "Script hash {} does not match address hash {}",
447                        hex::encode(computed_hash.as_byte_array()),
448                        hex::encode(script_hash)
449                    )));
450                }
451
452                // Parse the redeem script to extract public keys and threshold
453                // Expected format for multisig: OP_M <pubkey1> <pubkey2> ... <pubkeyN> OP_N OP_CHECKMULTISIG
454                let (threshold, pubkeys) = Self::parse_multisig_script(&script)?;
455
456                // Filter out empty signatures (OP_0 placeholders for CHECKMULTISIG bug)
457                let valid_signatures: Vec<_> = signatures
458                    .iter()
459                    .filter(|sig| !sig.is_empty() && sig.as_slice() != [0x00])
460                    .collect();
461
462                if valid_signatures.len() < threshold {
463                    return Err(ProtocolError::AddressWitnessError(format!(
464                        "Not enough signatures: got {}, need {}",
465                        valid_signatures.len(),
466                        threshold
467                    )));
468                }
469
470                // Verify signatures against public keys
471                // In standard multisig, signatures must match public keys in order
472                let mut sig_idx = 0;
473                let mut pubkey_idx = 0;
474                let mut matched = 0;
475                let mut signature_verifications: u16 = 0;
476
477                let signable_bytes_hash = sha256d::Hash::hash(signable_bytes).to_byte_array();
478                let msg = Message::from_digest(signable_bytes_hash);
479                let secp = Secp256k1::new();
480
481                while sig_idx < valid_signatures.len() && pubkey_idx < pubkeys.len() {
482                    signature_verifications += 1;
483
484                    let sig = RecoverableSignature::from_compact_signature(
485                        valid_signatures[sig_idx].as_slice(),
486                    )
487                    .map_err(|e| {
488                        ProtocolError::AddressWitnessError(format!(
489                            "Invalid signature format: {}",
490                            e
491                        ))
492                    })?;
493
494                    let pub_key = PublicKey::from_slice(&pubkeys[pubkey_idx]).map_err(|e| {
495                        ProtocolError::AddressWitnessError(format!("Invalid public key: {}", e))
496                    })?;
497
498                    if secp
499                        .verify_ecdsa(&msg, &sig.to_standard(), &pub_key.inner)
500                        .is_ok()
501                    {
502                        matched += 1;
503                        sig_idx += 1;
504                    }
505                    pubkey_idx += 1;
506                }
507
508                if matched >= threshold {
509                    Ok(AddressWitnessVerificationOperations::for_p2sh_multisig(
510                        signature_verifications,
511                        signable_bytes.len(),
512                    ))
513                } else {
514                    Err(ProtocolError::AddressWitnessError(format!(
515                        "Not enough valid signatures: verified {}, need {}",
516                        matched, threshold
517                    )))
518                }
519            }
520            (PlatformAddress::P2pkh(_), AddressWitness::P2sh { .. }) => {
521                Err(ProtocolError::AddressWitnessError(
522                    "P2PKH address requires P2pkh witness, got P2sh".to_string(),
523                ))
524            }
525            (PlatformAddress::P2sh(_), AddressWitness::P2pkh { .. }) => {
526                Err(ProtocolError::AddressWitnessError(
527                    "P2SH address requires P2sh witness, got P2pkh".to_string(),
528                ))
529            }
530        }
531    }
532
533    /// Parses a multisig redeem script and extracts the threshold (M) and public keys.
534    ///
535    /// Expected format: OP_M <pubkey1> <pubkey2> ... <pubkeyN> OP_N OP_CHECKMULTISIG
536    ///
537    /// # Supported Scripts
538    ///
539    /// Currently only standard bare multisig scripts are supported. Other P2SH script types
540    /// (timelocks, hash puzzles, custom scripts) are not supported and will return an error.
541    ///
542    /// Full script execution would require either:
543    /// - Using the `bitcoinconsensus` library with a synthetic spending transaction
544    /// - Implementing a complete script interpreter
545    ///
546    /// For Platform's authorization use cases, multisig is the primary expected P2SH pattern.
547    fn parse_multisig_script(script: &ScriptBuf) -> Result<(usize, Vec<Vec<u8>>), ProtocolError> {
548        use dashcore::blockdata::opcodes::all::*;
549
550        let mut instructions = script.instructions();
551        let mut pubkeys = Vec::new();
552
553        // First instruction should be OP_M (threshold)
554        let threshold = match instructions.next() {
555            Some(Ok(dashcore::blockdata::script::Instruction::Op(op))) => {
556                let byte = op.to_u8();
557                if byte >= OP_PUSHNUM_1.to_u8() && byte <= OP_PUSHNUM_16.to_u8() {
558                    (byte - OP_PUSHNUM_1.to_u8() + 1) as usize
559                } else {
560                    return Err(ProtocolError::AddressWitnessError(format!(
561                        "Unsupported P2SH script type: only standard multisig (OP_M ... OP_N OP_CHECKMULTISIG) is supported. \
562                         First opcode was 0x{:02x}, expected OP_1 through OP_16",
563                        byte
564                    )));
565                }
566            }
567            Some(Ok(dashcore::blockdata::script::Instruction::PushBytes(_))) => {
568                return Err(ProtocolError::AddressWitnessError(
569                    "Unsupported P2SH script type: only standard multisig is supported. \
570                     Script starts with a data push instead of OP_M threshold."
571                        .to_string(),
572                ))
573            }
574            Some(Err(e)) => {
575                return Err(ProtocolError::AddressWitnessError(format!(
576                    "Error parsing P2SH script: {:?}",
577                    e
578                )))
579            }
580            None => {
581                return Err(ProtocolError::AddressWitnessError(
582                    "Empty P2SH redeem script".to_string(),
583                ))
584            }
585        };
586
587        // Read public keys until we hit OP_N
588        loop {
589            match instructions.next() {
590                Some(Ok(dashcore::blockdata::script::Instruction::PushBytes(bytes))) => {
591                    // Only compressed public keys (33 bytes) are allowed
592                    let len = bytes.len();
593                    if len != 33 {
594                        return Err(ProtocolError::UncompressedPublicKeyNotAllowedError(
595                            crate::consensus::signature::UncompressedPublicKeyNotAllowedError::new(
596                                len,
597                            ),
598                        ));
599                    }
600                    pubkeys.push(bytes.as_bytes().to_vec());
601                }
602                Some(Ok(dashcore::blockdata::script::Instruction::Op(op))) => {
603                    let byte = op.to_u8();
604                    if byte >= OP_PUSHNUM_1.to_u8() && byte <= OP_PUSHNUM_16.to_u8() {
605                        // This is OP_N, the total number of keys
606                        let n = (byte - OP_PUSHNUM_1.to_u8() + 1) as usize;
607                        if pubkeys.len() != n {
608                            return Err(ProtocolError::AddressWitnessError(format!(
609                                "Multisig script declares {} keys but contains {}",
610                                n,
611                                pubkeys.len()
612                            )));
613                        }
614                        break;
615                    } else if op == OP_CHECKMULTISIG || op == OP_CHECKMULTISIGVERIFY {
616                        // Hit CHECKMULTISIG without seeing OP_N - malformed
617                        return Err(ProtocolError::AddressWitnessError(
618                            "Malformed multisig script: OP_CHECKMULTISIG before OP_N".to_string(),
619                        ));
620                    } else {
621                        return Err(ProtocolError::AddressWitnessError(format!(
622                            "Unsupported opcode 0x{:02x} in P2SH script. Only standard multisig is supported.",
623                            byte
624                        )));
625                    }
626                }
627                Some(Err(e)) => {
628                    return Err(ProtocolError::AddressWitnessError(format!(
629                        "Error parsing multisig script: {:?}",
630                        e
631                    )))
632                }
633                None => {
634                    return Err(ProtocolError::AddressWitnessError(
635                        "Incomplete multisig script: unexpected end before OP_N".to_string(),
636                    ))
637                }
638            }
639        }
640
641        // Validate threshold
642        if threshold > pubkeys.len() {
643            return Err(ProtocolError::AddressWitnessError(format!(
644                "Invalid multisig: threshold {} exceeds number of keys {}",
645                threshold,
646                pubkeys.len()
647            )));
648        }
649
650        // Next should be OP_CHECKMULTISIG
651        match instructions.next() {
652            Some(Ok(dashcore::blockdata::script::Instruction::Op(op))) => {
653                if op == OP_CHECKMULTISIG {
654                    // Standard multisig - verify script is complete
655                    if instructions.next().is_some() {
656                        return Err(ProtocolError::AddressWitnessError(
657                            "Multisig script has extra data after OP_CHECKMULTISIG".to_string(),
658                        ));
659                    }
660                    Ok((threshold, pubkeys))
661                } else if op == OP_CHECKMULTISIGVERIFY {
662                    Err(ProtocolError::AddressWitnessError(
663                        "OP_CHECKMULTISIGVERIFY is not supported, only OP_CHECKMULTISIG"
664                            .to_string(),
665                    ))
666                } else {
667                    Err(ProtocolError::AddressWitnessError(format!(
668                        "Expected OP_CHECKMULTISIG, got opcode 0x{:02x}",
669                        op.to_u8()
670                    )))
671                }
672            }
673            _ => Err(ProtocolError::AddressWitnessError(
674                "Invalid multisig script: expected OP_CHECKMULTISIG after OP_N".to_string(),
675            )),
676        }
677    }
678}
679
680impl std::fmt::Display for PlatformAddress {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682        match self {
683            PlatformAddress::P2pkh(hash) => write!(f, "P2PKH({})", hex::encode(hash)),
684            PlatformAddress::P2sh(hash) => write!(f, "P2SH({})", hex::encode(hash)),
685        }
686    }
687}
688
689/// Error type for parsing a bech32m-encoded Platform address
690#[derive(Debug, Clone, PartialEq, Eq)]
691pub struct PlatformAddressParseError(pub String);
692
693impl std::fmt::Display for PlatformAddressParseError {
694    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695        write!(f, "{}", self.0)
696    }
697}
698
699impl std::error::Error for PlatformAddressParseError {}
700
701impl FromStr for PlatformAddress {
702    type Err = PlatformAddressParseError;
703
704    /// Parses a bech32m-encoded Platform address string.
705    ///
706    /// This accepts addresses with either mainnet ("dash") or testnet ("tdash") HRP.
707    ///
708    /// # Example
709    /// ```ignore
710    /// let address: PlatformAddress = "dash1k...".parse()?;
711    /// ```
712    fn from_str(s: &str) -> Result<Self, Self::Err> {
713        Self::from_bech32m_string(s).map_err(|e| PlatformAddressParseError(e.to_string()))
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use dashcore::blockdata::opcodes::all::*;
721    use dashcore::hashes::Hash;
722    use dashcore::secp256k1::{PublicKey as RawPublicKey, Secp256k1, SecretKey as RawSecretKey};
723    use dashcore::PublicKey;
724    use platform_value::BinaryData;
725
726    /// All non-mainnet networks share the `tdash` HRP (DIP-0018), so an address parsed from a
727    /// bech32m string can only ever report Mainnet or Testnet — never Devnet/Regtest. Network
728    /// checks against a parsed address MUST therefore compare HRPs, not raw `Network` values
729    /// (`PlatformWallet::shielded_unshield_to` relies on this; comparing raw networks made every
730    /// unshield fail on devnet wallets).
731    #[test]
732    fn hrp_is_shared_across_all_non_mainnet_networks() {
733        let testnet = PlatformAddress::hrp_for_network(Network::Testnet);
734        assert_eq!(testnet, PLATFORM_HRP_TESTNET);
735        assert_eq!(PlatformAddress::hrp_for_network(Network::Devnet), testnet);
736        assert_eq!(PlatformAddress::hrp_for_network(Network::Regtest), testnet);
737        assert_ne!(
738            PlatformAddress::hrp_for_network(Network::Mainnet),
739            testnet,
740            "mainnet must use a distinct HRP"
741        );
742    }
743
744    /// Helper to create a keypair from a 32-byte seed
745    fn create_keypair(seed: [u8; 32]) -> (RawSecretKey, PublicKey) {
746        let secp = Secp256k1::new();
747        let secret_key = RawSecretKey::from_byte_array(&seed).expect("valid secret key");
748        let raw_public_key = RawPublicKey::from_secret_key(&secp, &secret_key);
749        let public_key = PublicKey::new(raw_public_key);
750        (secret_key, public_key)
751    }
752
753    /// Helper to sign data with a secret key
754    fn sign_data(data: &[u8], secret_key: &RawSecretKey) -> Vec<u8> {
755        dashcore::signer::sign(data, secret_key.as_ref())
756            .expect("signing should succeed")
757            .to_vec()
758    }
759
760    /// Creates a standard multisig redeem script: OP_M <pubkey1> ... <pubkeyN> OP_N OP_CHECKMULTISIG
761    fn create_multisig_script(threshold: u8, pubkeys: &[PublicKey]) -> Vec<u8> {
762        let mut script = Vec::new();
763
764        // OP_M (threshold)
765        script.push(OP_PUSHNUM_1.to_u8() + threshold - 1);
766
767        // Push each public key (33 bytes each for compressed)
768        for pubkey in pubkeys {
769            let bytes = pubkey.to_bytes();
770            script.push(bytes.len() as u8); // push length
771            script.extend_from_slice(&bytes);
772        }
773
774        // OP_N (total keys)
775        script.push(OP_PUSHNUM_1.to_u8() + pubkeys.len() as u8 - 1);
776
777        // OP_CHECKMULTISIG
778        script.push(OP_CHECKMULTISIG.to_u8());
779
780        script
781    }
782
783    #[test]
784    fn test_platform_address_from_private_key() {
785        // Create a keypair
786        let seed = [1u8; 32];
787        let (secret_key, public_key) = create_keypair(seed);
788
789        // Create PrivateKey from the secret key
790        let private_key = PrivateKey::new(secret_key, Network::Testnet);
791
792        // Derive address using From<&PrivateKey>
793        let address_from_private = PlatformAddress::from(&private_key);
794
795        // Derive address manually using pubkey_hash() which computes Hash160(pubkey)
796        // Hash160 = RIPEMD160(SHA256(x)), the standard Bitcoin P2PKH derivation
797        let pubkey_hash = public_key.pubkey_hash();
798        let address_from_pubkey = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array());
799
800        // Both addresses should be identical
801        assert_eq!(
802            address_from_private, address_from_pubkey,
803            "Address derived from private key should match Hash160(compressed_pubkey)"
804        );
805
806        // Verify it's a P2PKH address
807        assert!(address_from_private.is_p2pkh());
808    }
809
810    #[test]
811    fn test_p2pkh_verify_signature_success() {
812        // Create a keypair
813        let seed = [1u8; 32];
814        let (secret_key, public_key) = create_keypair(seed);
815
816        // Create P2PKH address from public key hash
817        let pubkey_hash = public_key.pubkey_hash();
818        let address = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array());
819
820        // Data to sign
821        let signable_bytes = b"test message for P2PKH verification";
822
823        // Sign the data
824        let signature = sign_data(signable_bytes, &secret_key);
825
826        // Create witness (only signature needed - public key is recovered)
827        let witness = AddressWitness::P2pkh {
828            signature: BinaryData::new(signature),
829        };
830
831        // Verify should succeed
832        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
833        assert!(
834            result.is_ok(),
835            "P2PKH verification should succeed: {:?}",
836            result
837        );
838    }
839
840    #[test]
841    fn test_p2pkh_verify_wrong_signature_fails() {
842        // Create a keypair
843        let seed = [1u8; 32];
844        let (secret_key, public_key) = create_keypair(seed);
845
846        // Create P2PKH address from public key hash
847        let pubkey_hash = public_key.pubkey_hash();
848        let address = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array());
849
850        // Sign different data than what we verify
851        let sign_bytes = b"original message";
852        let verify_bytes = b"different message";
853        let signature = sign_data(sign_bytes, &secret_key);
854
855        // Create witness with signature for different data
856        let witness = AddressWitness::P2pkh {
857            signature: BinaryData::new(signature),
858        };
859
860        // Verify should fail (recovered pubkey won't match because message differs)
861        let result = address.verify_bytes_against_witness(&witness, verify_bytes);
862        assert!(
863            result.is_err(),
864            "P2PKH verification should fail with wrong data"
865        );
866    }
867
868    #[test]
869    fn test_p2pkh_verify_wrong_key_fails() {
870        // Create two keypairs
871        let seed1 = [1u8; 32];
872        let seed2 = [2u8; 32];
873        let (_secret_key1, public_key1) = create_keypair(seed1);
874        let (secret_key2, _public_key2) = create_keypair(seed2);
875
876        // Create P2PKH address from public key 1's hash
877        let pubkey_hash = public_key1.pubkey_hash();
878        let address = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array());
879
880        // Sign with key 2 (wrong key)
881        let signable_bytes = b"test message";
882        let signature = sign_data(signable_bytes, &secret_key2);
883
884        // Create witness (signature is from key 2, but address is for key 1)
885        let witness = AddressWitness::P2pkh {
886            signature: BinaryData::new(signature),
887        };
888
889        // Verify should fail (recovered pubkey hash won't match address)
890        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
891        assert!(
892            result.is_err(),
893            "P2PKH verification should fail when signed with wrong key"
894        );
895    }
896
897    // NOTE: test_uncompressed_public_key_rejected was removed because P2PKH witnesses
898    // no longer include the public key - it's recovered from the signature during verification.
899    // ECDSA recovery always produces a compressed public key (33 bytes).
900
901    #[test]
902    fn test_p2sh_2_of_3_multisig_verify_success() {
903        // Create 3 keypairs for 2-of-3 multisig
904        let seeds: [[u8; 32]; 3] = [[1u8; 32], [2u8; 32], [3u8; 32]];
905        let keypairs: Vec<_> = seeds.iter().map(|s| create_keypair(*s)).collect();
906        let pubkeys: Vec<_> = keypairs.iter().map(|(_, pk)| *pk).collect();
907
908        // Create 2-of-3 multisig redeem script
909        let redeem_script = create_multisig_script(2, &pubkeys);
910
911        // Create P2SH address from script hash
912        let script_buf = ScriptBuf::from_bytes(redeem_script.clone());
913        let script_hash = script_buf.script_hash();
914        let address = PlatformAddress::P2sh(*script_hash.as_byte_array());
915
916        // Data to sign
917        let signable_bytes = b"test message for P2SH 2-of-3 multisig";
918
919        // Sign with first two keys (keys 0 and 1)
920        let sig0 = sign_data(signable_bytes, &keypairs[0].0);
921        let sig1 = sign_data(signable_bytes, &keypairs[1].0);
922
923        // Create witness with signatures in order
924        // Note: CHECKMULTISIG requires signatures in the same order as pubkeys
925        let witness = AddressWitness::P2sh {
926            signatures: vec![BinaryData::new(sig0), BinaryData::new(sig1)],
927            redeem_script: BinaryData::new(redeem_script),
928        };
929
930        // Verify should succeed
931        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
932        assert!(
933            result.is_ok(),
934            "P2SH 2-of-3 multisig verification should succeed: {:?}",
935            result
936        );
937    }
938
939    #[test]
940    fn test_p2sh_2_of_3_multisig_with_keys_1_and_2_success() {
941        // Create 3 keypairs for 2-of-3 multisig
942        let seeds: [[u8; 32]; 3] = [[1u8; 32], [2u8; 32], [3u8; 32]];
943        let keypairs: Vec<_> = seeds.iter().map(|s| create_keypair(*s)).collect();
944        let pubkeys: Vec<_> = keypairs.iter().map(|(_, pk)| *pk).collect();
945
946        // Create 2-of-3 multisig redeem script
947        let redeem_script = create_multisig_script(2, &pubkeys);
948
949        // Create P2SH address from script hash
950        let script_buf = ScriptBuf::from_bytes(redeem_script.clone());
951        let script_hash = script_buf.script_hash();
952        let address = PlatformAddress::P2sh(*script_hash.as_byte_array());
953
954        // Data to sign
955        let signable_bytes = b"test message for P2SH 2-of-3 multisig";
956
957        // Sign with keys 1 and 2 (different combination)
958        let sig1 = sign_data(signable_bytes, &keypairs[1].0);
959        let sig2 = sign_data(signable_bytes, &keypairs[2].0);
960
961        // Create witness with signatures in order
962        let witness = AddressWitness::P2sh {
963            signatures: vec![BinaryData::new(sig1), BinaryData::new(sig2)],
964            redeem_script: BinaryData::new(redeem_script),
965        };
966
967        // Verify should succeed
968        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
969        assert!(
970            result.is_ok(),
971            "P2SH 2-of-3 multisig with keys 1 and 2 should succeed: {:?}",
972            result
973        );
974    }
975
976    #[test]
977    fn test_p2sh_not_enough_signatures_fails() {
978        // Create 3 keypairs for 2-of-3 multisig
979        let seeds: [[u8; 32]; 3] = [[1u8; 32], [2u8; 32], [3u8; 32]];
980        let keypairs: Vec<_> = seeds.iter().map(|s| create_keypair(*s)).collect();
981        let pubkeys: Vec<_> = keypairs.iter().map(|(_, pk)| *pk).collect();
982
983        // Create 2-of-3 multisig redeem script
984        let redeem_script = create_multisig_script(2, &pubkeys);
985
986        // Create P2SH address from script hash
987        let script_buf = ScriptBuf::from_bytes(redeem_script.clone());
988        let script_hash = script_buf.script_hash();
989        let address = PlatformAddress::P2sh(*script_hash.as_byte_array());
990
991        // Data to sign
992        let signable_bytes = b"test message";
993
994        // Only sign with one key (need 2)
995        let sig0 = sign_data(signable_bytes, &keypairs[0].0);
996
997        // Create witness with only one signature
998        let witness = AddressWitness::P2sh {
999            signatures: vec![BinaryData::new(sig0)],
1000            redeem_script: BinaryData::new(redeem_script),
1001        };
1002
1003        // Verify should fail
1004        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
1005        assert!(
1006            result.is_err(),
1007            "P2SH should fail with only 1 signature when 2 required"
1008        );
1009        assert!(
1010            result.unwrap_err().to_string().contains("Not enough"),
1011            "Error should mention not enough signatures"
1012        );
1013    }
1014
1015    #[test]
1016    fn test_p2sh_wrong_script_hash_fails() {
1017        // Create 3 keypairs
1018        let seeds: [[u8; 32]; 3] = [[1u8; 32], [2u8; 32], [3u8; 32]];
1019        let keypairs: Vec<_> = seeds.iter().map(|s| create_keypair(*s)).collect();
1020        let pubkeys: Vec<_> = keypairs.iter().map(|(_, pk)| *pk).collect();
1021
1022        // Create a redeem script
1023        let redeem_script = create_multisig_script(2, &pubkeys);
1024
1025        // Create P2SH address with DIFFERENT hash (wrong address)
1026        let wrong_hash = [0xABu8; 20];
1027        let address = PlatformAddress::P2sh(wrong_hash);
1028
1029        // Data to sign
1030        let signable_bytes = b"test message";
1031
1032        // Sign correctly
1033        let sig0 = sign_data(signable_bytes, &keypairs[0].0);
1034        let sig1 = sign_data(signable_bytes, &keypairs[1].0);
1035
1036        // Create witness
1037        let witness = AddressWitness::P2sh {
1038            signatures: vec![BinaryData::new(sig0), BinaryData::new(sig1)],
1039            redeem_script: BinaryData::new(redeem_script),
1040        };
1041
1042        // Verify should fail (script doesn't hash to address)
1043        let result = address.verify_bytes_against_witness(&witness, signable_bytes);
1044        assert!(
1045            result.is_err(),
1046            "P2SH should fail when script hash doesn't match address"
1047        );
1048        assert!(
1049            result
1050                .unwrap_err()
1051                .to_string()
1052                .contains("does not match address hash"),
1053            "Error should mention hash mismatch"
1054        );
1055    }
1056
1057    #[test]
1058    fn test_p2pkh_and_p2sh_together() {
1059        // This test simulates having both a P2PKH and P2SH output and redeeming both
1060
1061        // === P2PKH Output ===
1062        let p2pkh_seed = [10u8; 32];
1063        let (p2pkh_secret, p2pkh_pubkey) = create_keypair(p2pkh_seed);
1064        let p2pkh_hash = p2pkh_pubkey.pubkey_hash();
1065        let p2pkh_address = PlatformAddress::P2pkh(*p2pkh_hash.as_byte_array());
1066
1067        // === P2SH Output (2-of-3 multisig) ===
1068        let p2sh_seeds: [[u8; 32]; 3] = [[20u8; 32], [21u8; 32], [22u8; 32]];
1069        let p2sh_keypairs: Vec<_> = p2sh_seeds.iter().map(|s| create_keypair(*s)).collect();
1070        let p2sh_pubkeys: Vec<_> = p2sh_keypairs.iter().map(|(_, pk)| *pk).collect();
1071        let redeem_script = create_multisig_script(2, &p2sh_pubkeys);
1072        let script_buf = ScriptBuf::from_bytes(redeem_script.clone());
1073        let script_hash = script_buf.script_hash();
1074        let p2sh_address = PlatformAddress::P2sh(*script_hash.as_byte_array());
1075
1076        // === Signable bytes (same for both in this test) ===
1077        let signable_bytes = b"combined transaction data to redeem both outputs";
1078
1079        // === Redeem P2PKH ===
1080        let p2pkh_sig = sign_data(signable_bytes, &p2pkh_secret);
1081        let p2pkh_witness = AddressWitness::P2pkh {
1082            signature: BinaryData::new(p2pkh_sig),
1083        };
1084        let p2pkh_result =
1085            p2pkh_address.verify_bytes_against_witness(&p2pkh_witness, signable_bytes);
1086        assert!(
1087            p2pkh_result.is_ok(),
1088            "P2PKH redemption should succeed: {:?}",
1089            p2pkh_result
1090        );
1091
1092        // === Redeem P2SH (using keys 0 and 2) ===
1093        let p2sh_sig0 = sign_data(signable_bytes, &p2sh_keypairs[0].0);
1094        let p2sh_sig2 = sign_data(signable_bytes, &p2sh_keypairs[2].0);
1095        let p2sh_witness = AddressWitness::P2sh {
1096            signatures: vec![BinaryData::new(p2sh_sig0), BinaryData::new(p2sh_sig2)],
1097            redeem_script: BinaryData::new(redeem_script),
1098        };
1099        let p2sh_result = p2sh_address.verify_bytes_against_witness(&p2sh_witness, signable_bytes);
1100        assert!(
1101            p2sh_result.is_ok(),
1102            "P2SH redemption should succeed: {:?}",
1103            p2sh_result
1104        );
1105
1106        // Both outputs successfully redeemed!
1107    }
1108
1109    #[test]
1110    fn test_witness_type_mismatch() {
1111        // Create P2PKH address
1112        let seed = [1u8; 32];
1113        let (_, public_key) = create_keypair(seed);
1114        let pubkey_hash = public_key.pubkey_hash();
1115        let p2pkh_address = PlatformAddress::P2pkh(*pubkey_hash.as_byte_array());
1116
1117        // Create P2SH address
1118        let p2sh_hash = [0xABu8; 20];
1119        let p2sh_address = PlatformAddress::P2sh(p2sh_hash);
1120
1121        let signable_bytes = b"test data";
1122
1123        // Try P2SH witness on P2PKH address
1124        let p2sh_witness = AddressWitness::P2sh {
1125            signatures: vec![BinaryData::new(vec![0x30, 0x44])],
1126            redeem_script: BinaryData::new(vec![0x52]),
1127        };
1128        let result = p2pkh_address.verify_bytes_against_witness(&p2sh_witness, signable_bytes);
1129        assert!(result.is_err());
1130        assert!(result
1131            .unwrap_err()
1132            .to_string()
1133            .contains("P2PKH address requires P2pkh witness"));
1134
1135        // Try P2PKH witness on P2SH address
1136        let p2pkh_witness = AddressWitness::P2pkh {
1137            signature: BinaryData::new(vec![0x30, 0x44]),
1138        };
1139        let result = p2sh_address.verify_bytes_against_witness(&p2pkh_witness, signable_bytes);
1140        assert!(result.is_err());
1141        assert!(result
1142            .unwrap_err()
1143            .to_string()
1144            .contains("P2SH address requires P2sh witness"));
1145    }
1146
1147    // ========================
1148    // Bech32m encoding tests (DIP-0018)
1149    // ========================
1150
1151    #[test]
1152    fn test_bech32m_p2pkh_mainnet_roundtrip() {
1153        // Test P2PKH address roundtrip on mainnet
1154        let hash: [u8; 20] = [
1155            0xf7, 0xda, 0x0a, 0x2b, 0x5c, 0xbd, 0x4f, 0xf6, 0xbb, 0x2c, 0x4d, 0x89, 0xb6, 0x7d,
1156            0x2f, 0x3f, 0xfe, 0xec, 0x05, 0x25,
1157        ];
1158        let address = PlatformAddress::P2pkh(hash);
1159
1160        // Encode to bech32m
1161        let encoded = address.to_bech32m_string(Network::Mainnet);
1162
1163        // Verify exact encoding
1164        assert_eq!(
1165            encoded, "dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs",
1166            "P2PKH mainnet encoding mismatch"
1167        );
1168
1169        // Decode and verify roundtrip
1170        let decoded =
1171            PlatformAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
1172        assert_eq!(decoded, address);
1173    }
1174
1175    #[test]
1176    fn test_bech32m_p2pkh_testnet_roundtrip() {
1177        // Test P2PKH address roundtrip on testnet
1178        let hash: [u8; 20] = [
1179            0xf7, 0xda, 0x0a, 0x2b, 0x5c, 0xbd, 0x4f, 0xf6, 0xbb, 0x2c, 0x4d, 0x89, 0xb6, 0x7d,
1180            0x2f, 0x3f, 0xfe, 0xec, 0x05, 0x25,
1181        ];
1182        let address = PlatformAddress::P2pkh(hash);
1183
1184        // Encode to bech32m
1185        let encoded = address.to_bech32m_string(Network::Testnet);
1186
1187        // Verify exact encoding
1188        assert_eq!(
1189            encoded, "tdash1krma5z3ttj75la4m93xcndna9ullamq9y5fzq2j7",
1190            "P2PKH testnet encoding mismatch"
1191        );
1192
1193        // Decode and verify roundtrip
1194        let decoded =
1195            PlatformAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
1196        assert_eq!(decoded, address);
1197    }
1198
1199    #[test]
1200    fn test_bech32m_p2sh_mainnet_roundtrip() {
1201        // Test P2SH address roundtrip on mainnet
1202        let hash: [u8; 20] = [
1203            0x43, 0xfa, 0x18, 0x3c, 0xf3, 0xfb, 0x6e, 0x9e, 0x7d, 0xc6, 0x2b, 0x69, 0x2a, 0xeb,
1204            0x4f, 0xc8, 0xd8, 0x04, 0x56, 0x36,
1205        ];
1206        let address = PlatformAddress::P2sh(hash);
1207
1208        // Encode to bech32m
1209        let encoded = address.to_bech32m_string(Network::Mainnet);
1210
1211        // Verify exact encoding
1212        assert_eq!(
1213            encoded, "dash1sppl5xpu70aka8nacc4kj2htflydspzkxch4cad6",
1214            "P2SH mainnet encoding mismatch"
1215        );
1216
1217        // Decode and verify roundtrip
1218        let decoded =
1219            PlatformAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
1220        assert_eq!(decoded, address);
1221    }
1222
1223    #[test]
1224    fn test_bech32m_p2sh_testnet_roundtrip() {
1225        // Test P2SH address roundtrip on testnet
1226        let hash: [u8; 20] = [
1227            0x43, 0xfa, 0x18, 0x3c, 0xf3, 0xfb, 0x6e, 0x9e, 0x7d, 0xc6, 0x2b, 0x69, 0x2a, 0xeb,
1228            0x4f, 0xc8, 0xd8, 0x04, 0x56, 0x36,
1229        ];
1230        let address = PlatformAddress::P2sh(hash);
1231
1232        // Encode to bech32m
1233        let encoded = address.to_bech32m_string(Network::Testnet);
1234
1235        // Verify exact encoding
1236        assert_eq!(
1237            encoded, "tdash1sppl5xpu70aka8nacc4kj2htflydspzkxc8jtru5",
1238            "P2SH testnet encoding mismatch"
1239        );
1240
1241        // Decode and verify roundtrip
1242        let decoded =
1243            PlatformAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
1244        assert_eq!(decoded, address);
1245    }
1246
1247    #[test]
1248    fn test_bech32m_devnet_uses_testnet_hrp() {
1249        let hash: [u8; 20] = [0xAB; 20];
1250        let address = PlatformAddress::P2pkh(hash);
1251
1252        // Devnet should use testnet HRP
1253        let encoded = address.to_bech32m_string(Network::Devnet);
1254        assert!(
1255            encoded.starts_with("tdash1"),
1256            "Devnet address should start with 'tdash1', got: {}",
1257            encoded
1258        );
1259    }
1260
1261    #[test]
1262    fn test_bech32m_regtest_uses_testnet_hrp() {
1263        let hash: [u8; 20] = [0xAB; 20];
1264        let address = PlatformAddress::P2pkh(hash);
1265
1266        // Regtest should use testnet HRP
1267        let encoded = address.to_bech32m_string(Network::Regtest);
1268        assert!(
1269            encoded.starts_with("tdash1"),
1270            "Regtest address should start with 'tdash1', got: {}",
1271            encoded
1272        );
1273    }
1274
1275    #[test]
1276    fn test_bech32m_invalid_hrp_fails() {
1277        let wrong_hrp = Hrp::parse("bitcoin").unwrap();
1278        let payload: [u8; 21] = [0x00; 21];
1279        let wrong_hrp_address = bech32::encode::<Bech32m>(wrong_hrp, &payload).unwrap();
1280
1281        let result = PlatformAddress::from_bech32m_string(&wrong_hrp_address);
1282        assert!(result.is_err());
1283        let err = result.unwrap_err();
1284        assert!(
1285            err.to_string().contains("not a platform address"),
1286            "Error should mention non-platform HRP: {}",
1287            err
1288        );
1289    }
1290
1291    #[test]
1292    fn test_bech32m_invalid_checksum_fails() {
1293        // Create a valid address, then corrupt the checksum
1294        let hash: [u8; 20] = [0xAB; 20];
1295        let address = PlatformAddress::P2pkh(hash);
1296        let mut encoded = address.to_bech32m_string(Network::Mainnet);
1297
1298        // Corrupt the last character (part of checksum)
1299        let last_char = encoded.pop().unwrap();
1300        let corrupted_char = if last_char == 'q' { 'p' } else { 'q' };
1301        encoded.push(corrupted_char);
1302
1303        let result = PlatformAddress::from_bech32m_string(&encoded);
1304        assert!(result.is_err(), "Should fail with corrupted checksum");
1305    }
1306
1307    #[test]
1308    fn test_bech32m_invalid_type_byte_fails() {
1309        // Manually construct an address with invalid type byte (0x02)
1310        // We need to use the bech32 crate directly for this
1311        let hrp = Hrp::parse("dash").unwrap();
1312        let invalid_payload: [u8; 21] = [0x02; 21]; // type byte 0x02 is invalid
1313        let encoded = bech32::encode::<Bech32m>(hrp, &invalid_payload).unwrap();
1314
1315        let result = PlatformAddress::from_bech32m_string(&encoded);
1316        assert!(result.is_err());
1317        let err = result.unwrap_err();
1318        assert!(
1319            err.to_string().contains("invalid address type"),
1320            "Error should mention invalid type: {}",
1321            err
1322        );
1323    }
1324
1325    #[test]
1326    fn test_bech32m_too_short_fails() {
1327        // Construct an address with too few bytes
1328        let hrp = Hrp::parse("dash").unwrap();
1329        let short_payload: [u8; 10] = [0xb0; 10]; // Only 10 bytes instead of 21
1330        let encoded = bech32::encode::<Bech32m>(hrp, &short_payload).unwrap();
1331
1332        let result = PlatformAddress::from_bech32m_string(&encoded);
1333        assert!(result.is_err());
1334        let err = result.unwrap_err();
1335        assert!(
1336            err.to_string().contains("invalid Platform address length"),
1337            "Error should mention invalid length: {}",
1338            err
1339        );
1340    }
1341
1342    #[test]
1343    fn test_bech32m_from_str_trait() {
1344        // Test the FromStr trait implementation
1345        let hash: [u8; 20] = [
1346            0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
1347            0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc,
1348        ];
1349        let original = PlatformAddress::P2pkh(hash);
1350
1351        // Encode and then parse via FromStr
1352        let encoded = original.to_bech32m_string(Network::Testnet);
1353        let parsed: PlatformAddress = encoded.parse().expect("parsing should succeed");
1354
1355        assert_eq!(parsed, original);
1356    }
1357
1358    #[test]
1359    fn test_bech32m_case_insensitive() {
1360        // Per DIP-0018, addresses must be lowercase or uppercase (not mixed)
1361        // The bech32 crate should handle this
1362        let hash: [u8; 20] = [0xAB; 20];
1363        let address = PlatformAddress::P2pkh(hash);
1364
1365        let lowercase = address.to_bech32m_string(Network::Mainnet);
1366        let uppercase = lowercase.to_uppercase();
1367
1368        // Both should decode to the same address
1369        let decoded_lower = PlatformAddress::from_bech32m_string(&lowercase).unwrap();
1370        let decoded_upper = PlatformAddress::from_bech32m_string(&uppercase).unwrap();
1371
1372        assert_eq!(decoded_lower, decoded_upper);
1373        assert_eq!(decoded_lower, address);
1374    }
1375
1376    #[test]
1377    fn test_bech32m_all_zeros_p2pkh() {
1378        // Edge case: all-zero hash
1379        let address = PlatformAddress::P2pkh([0u8; 20]);
1380        let encoded = address.to_bech32m_string(Network::Mainnet);
1381        let decoded = PlatformAddress::from_bech32m_string(&encoded).unwrap();
1382        assert_eq!(decoded, address);
1383    }
1384
1385    #[test]
1386    fn test_bech32m_all_ones_p2sh() {
1387        // Edge case: all-ones hash
1388        let address = PlatformAddress::P2sh([0xFF; 20]);
1389        let encoded = address.to_bech32m_string(Network::Mainnet);
1390        let decoded = PlatformAddress::from_bech32m_string(&encoded).unwrap();
1391        assert_eq!(decoded, address);
1392    }
1393
1394    #[test]
1395    fn test_hrp_for_network() {
1396        assert_eq!(PlatformAddress::hrp_for_network(Network::Mainnet), "dash");
1397        assert_eq!(PlatformAddress::hrp_for_network(Network::Testnet), "tdash");
1398        assert_eq!(PlatformAddress::hrp_for_network(Network::Devnet), "tdash");
1399        assert_eq!(PlatformAddress::hrp_for_network(Network::Regtest), "tdash");
1400    }
1401
1402    #[test]
1403    fn test_storage_bytes_format() {
1404        // Verify that to_bytes() (using bincode) produces expected format:
1405        // [variant_index (1 byte)] + [hash (20 bytes)]
1406        // P2pkh = variant 0, P2sh = variant 1
1407        let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
1408        let p2sh = PlatformAddress::P2sh([0xCD; 20]);
1409
1410        let p2pkh_bytes = p2pkh.to_bytes();
1411        let p2sh_bytes = p2sh.to_bytes();
1412
1413        // Verify format: 21 bytes total, first byte is variant index
1414        assert_eq!(p2pkh_bytes.len(), 21);
1415        assert_eq!(p2sh_bytes.len(), 21);
1416        assert_eq!(p2pkh_bytes[0], 0x00, "P2pkh variant index must be 0x00");
1417        assert_eq!(p2sh_bytes[0], 0x01, "P2sh variant index must be 0x01");
1418
1419        // Verify roundtrip through from_bytes
1420        let p2pkh_decoded = PlatformAddress::from_bytes(&p2pkh_bytes).unwrap();
1421        let p2sh_decoded = PlatformAddress::from_bytes(&p2sh_bytes).unwrap();
1422        assert_eq!(p2pkh_decoded, p2pkh);
1423        assert_eq!(p2sh_decoded, p2sh);
1424    }
1425
1426    #[test]
1427    fn test_bech32m_uses_different_type_bytes_than_storage() {
1428        // Verify that bech32m encoding uses type bytes (0xb0/0x80)
1429        // while storage (bincode) uses variant indices (0x00/0x01)
1430        let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
1431        let p2sh = PlatformAddress::P2sh([0xCD; 20]);
1432
1433        // Storage bytes (bincode) use variant indices 0x00/0x01
1434        assert_eq!(p2pkh.to_bytes()[0], 0x00);
1435        assert_eq!(p2sh.to_bytes()[0], 0x01);
1436
1437        // Bech32m encoding uses 0xb0/0xb8 (verified by successful roundtrip)
1438        let p2pkh_encoded = p2pkh.to_bech32m_string(Network::Mainnet);
1439        let p2sh_encoded = p2sh.to_bech32m_string(Network::Mainnet);
1440
1441        let p2pkh_decoded = PlatformAddress::from_bech32m_string(&p2pkh_encoded).unwrap();
1442        let p2sh_decoded = PlatformAddress::from_bech32m_string(&p2sh_encoded).unwrap();
1443
1444        assert_eq!(p2pkh_decoded, p2pkh);
1445        assert_eq!(p2sh_decoded, p2sh);
1446    }
1447
1448    #[test]
1449    fn test_is_mainnet_bech32m_mainnet_is_true() {
1450        let encoded = PlatformAddress::P2pkh([0x11; 20]).to_bech32m_string(Network::Mainnet);
1451        assert!(encoded.starts_with("dash1"));
1452        assert!(PlatformAddress::is_mainnet_bech32m(&encoded).unwrap());
1453    }
1454
1455    #[test]
1456    fn test_is_mainnet_bech32m_all_non_mainnet_networks_are_false() {
1457        // Testnet, Devnet, and Regtest all share the `tdash` HRP, so all three
1458        // classify as non-mainnet (false) — the only truthful answer DIP-0018
1459        // allows from the address string alone.
1460        for network in [Network::Testnet, Network::Devnet, Network::Regtest] {
1461            let encoded = PlatformAddress::P2pkh([0x22; 20]).to_bech32m_string(network);
1462            assert!(encoded.starts_with("tdash1"), "network {network:?}");
1463            assert!(
1464                !PlatformAddress::is_mainnet_bech32m(&encoded).unwrap(),
1465                "network {network:?} must classify as non-mainnet"
1466            );
1467        }
1468    }
1469
1470    #[test]
1471    fn test_is_mainnet_bech32m_is_case_insensitive() {
1472        let mainnet = PlatformAddress::P2pkh([0x33; 20])
1473            .to_bech32m_string(Network::Mainnet)
1474            .to_uppercase();
1475        assert!(mainnet.starts_with("DASH1"));
1476        assert!(PlatformAddress::is_mainnet_bech32m(&mainnet).unwrap());
1477
1478        let testnet = PlatformAddress::P2pkh([0x44; 20])
1479            .to_bech32m_string(Network::Testnet)
1480            .to_uppercase();
1481        assert!(testnet.starts_with("TDASH1"));
1482        assert!(!PlatformAddress::is_mainnet_bech32m(&testnet).unwrap());
1483    }
1484
1485    #[test]
1486    fn test_is_mainnet_bech32m_non_platform_hrp_errors() {
1487        // Valid Bitcoin bech32 address: decode succeeds, HRP "bc" triggers error.
1488        let err = PlatformAddress::is_mainnet_bech32m("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
1489            .unwrap_err();
1490        assert!(
1491            err.to_string().contains("not a platform address"),
1492            "unexpected error: {err}"
1493        );
1494    }
1495
1496    #[test]
1497    fn test_is_mainnet_bech32m_malformed_data_part_errors() {
1498        // `dash1!` has a valid HRP but `!` is not a bech32 character.
1499        // Previously this returned Ok(true) (the HRP-only check); now it must
1500        // return an error because bech32::decode validates the full string.
1501        assert!(
1502            PlatformAddress::is_mainnet_bech32m("dash1!").is_err(),
1503            "dash1! must error, not return Ok(true)"
1504        );
1505    }
1506
1507    #[test]
1508    fn test_is_mainnet_bech32m_missing_separator_errors() {
1509        let err = PlatformAddress::is_mainnet_bech32m("nodelimiterhere").unwrap_err();
1510        // bech32::decode returns "parsing failed" for strings without separator
1511        assert!(
1512            err.to_string().contains("parsing failed") || err.to_string().contains("separator"),
1513            "unexpected error: {err}"
1514        );
1515    }
1516
1517    #[test]
1518    fn test_is_mainnet_bech32m_empty_errors() {
1519        let err = PlatformAddress::is_mainnet_bech32m("").unwrap_err();
1520        assert!(
1521            err.to_string().contains("parsing failed") || err.to_string().contains("separator"),
1522            "unexpected error: {err}"
1523        );
1524    }
1525}