dpp/address_funds/
orchard_address.rs

1use bech32::{Bech32m, Hrp};
2use dashcore::Network;
3
4use crate::address_funds::platform_address::{PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET};
5use crate::address_funds::PlatformAddress;
6use crate::ProtocolError;
7
8/// Size of the Orchard diversifier (11 bytes).
9pub const ORCHARD_DIVERSIFIER_SIZE: usize = 11;
10/// Size of the Orchard diversified transmission key pk_d (32 bytes, Pallas curve point).
11pub const ORCHARD_PKD_SIZE: usize = 32;
12/// Total size of a raw Orchard payment address (43 bytes = diversifier + pk_d).
13pub const ORCHARD_ADDRESS_SIZE: usize = ORCHARD_DIVERSIFIER_SIZE + ORCHARD_PKD_SIZE;
14
15/// An Orchard shielded payment address.
16///
17/// Composed of a diversifier (11 bytes) and a diversified transmission key (32 bytes).
18/// The diversifier enables a single spending key to derive an unlimited number of
19/// unlinkable payment addresses. Only the holder of the corresponding FullViewingKey
20/// (or IncomingViewingKey) can link diversified addresses to the same wallet.
21///
22/// Bech32m encoding uses type byte `0x10`, producing addresses that start with `z`:
23/// - Mainnet: `dash1z...`
24/// - Testnet: `tdash1z...`
25///
26/// The raw Orchard address format matches Zcash Orchard (43 bytes), but the
27/// string encoding is Dash-specific (no F4Jumble, no Unified Address wrapper).
28///
29/// Wraps `grovedb_commitment_tree::PaymentAddress`. Use [`From<PaymentAddress>`]
30/// to convert from the orchard crate's native type, or [`inner()`](OrchardAddress::inner)
31/// / [`into_inner()`](OrchardAddress::into_inner) to access the wrapped address.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct OrchardAddress(grovedb_commitment_tree::PaymentAddress);
34
35impl OrchardAddress {
36    /// Type byte for Orchard addresses in bech32m encoding (user-facing).
37    /// Produces 'z' as the first bech32 character.
38    pub const ORCHARD_TYPE: u8 = 0x10;
39
40    /// Returns the inner [`PaymentAddress`](grovedb_commitment_tree::PaymentAddress).
41    pub fn inner(&self) -> &grovedb_commitment_tree::PaymentAddress {
42        &self.0
43    }
44
45    /// Consumes the wrapper and returns the inner `PaymentAddress`.
46    pub fn into_inner(self) -> grovedb_commitment_tree::PaymentAddress {
47        self.0
48    }
49
50    /// Creates an OrchardAddress from a 43-byte raw address.
51    ///
52    /// The first 11 bytes are the diversifier, the next 32 are pk_d.
53    /// Returns an error if `pk_d` is not a valid Pallas curve point.
54    pub fn from_raw_bytes(bytes: &[u8; ORCHARD_ADDRESS_SIZE]) -> Result<Self, ProtocolError> {
55        let addr =
56            Option::from(grovedb_commitment_tree::PaymentAddress::from_raw_address_bytes(bytes))
57                .ok_or_else(|| {
58                    ProtocolError::DecodingError(
59                        "OrchardAddress pk_d is not a valid Pallas curve point".to_string(),
60                    )
61                })?;
62        Ok(Self(addr))
63    }
64
65    /// Returns the raw 43-byte address (diversifier || pk_d).
66    pub fn to_raw_bytes(&self) -> [u8; ORCHARD_ADDRESS_SIZE] {
67        self.0.to_raw_address_bytes()
68    }
69
70    /// Encodes the OrchardAddress as a bech32m string for the specified network.
71    ///
72    /// Format: `<HRP>1<data-part>`
73    /// - Data: type_byte (0x10) || diversifier (11 bytes) || pk_d (32 bytes)
74    /// - Total payload: 44 bytes
75    /// - Checksum: bech32m (BIP-350)
76    pub fn to_bech32m_string(&self, network: Network) -> String {
77        let hrp_str = PlatformAddress::hrp_for_network(network);
78        let hrp = Hrp::parse(hrp_str).expect("HRP is valid");
79
80        let raw = self.to_raw_bytes();
81        let mut payload = Vec::with_capacity(1 + ORCHARD_ADDRESS_SIZE);
82        payload.push(Self::ORCHARD_TYPE);
83        payload.extend_from_slice(&raw);
84
85        bech32::encode::<Bech32m>(hrp, &payload).expect("encoding should succeed")
86    }
87
88    /// Decodes a bech32m-encoded Orchard address string.
89    ///
90    /// # Returns
91    /// - `Ok((OrchardAddress, Network))` - The decoded address and its network
92    /// - `Err(ProtocolError)` - If the address is invalid
93    pub fn from_bech32m_string(s: &str) -> Result<(Self, Network), ProtocolError> {
94        let (hrp, data) =
95            bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
96
97        let hrp_lower = hrp.as_str().to_ascii_lowercase();
98        let network = match hrp_lower.as_str() {
99            s if s == PLATFORM_HRP_MAINNET => Network::Mainnet,
100            s if s == PLATFORM_HRP_TESTNET => Network::Testnet,
101            _ => {
102                return Err(ProtocolError::DecodingError(format!(
103                    "invalid HRP '{}': expected '{}' or '{}'",
104                    hrp, PLATFORM_HRP_MAINNET, PLATFORM_HRP_TESTNET
105                )))
106            }
107        };
108
109        // Validate payload: 1 type byte + 11 diversifier + 32 pk_d = 44 bytes
110        if data.len() != 1 + ORCHARD_ADDRESS_SIZE {
111            return Err(ProtocolError::DecodingError(format!(
112                "invalid Orchard address length: expected {} bytes, got {}",
113                1 + ORCHARD_ADDRESS_SIZE,
114                data.len()
115            )));
116        }
117
118        if data[0] != Self::ORCHARD_TYPE {
119            return Err(ProtocolError::DecodingError(format!(
120                "invalid Orchard address type byte: expected 0x{:02x}, got 0x{:02x}",
121                Self::ORCHARD_TYPE,
122                data[0]
123            )));
124        }
125
126        let mut raw = [0u8; ORCHARD_ADDRESS_SIZE];
127        raw.copy_from_slice(&data[1..]);
128        Self::from_raw_bytes(&raw).map(|addr| (addr, network))
129    }
130}
131
132/// Infallible conversion from the orchard crate's `PaymentAddress` to `OrchardAddress`.
133impl From<grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
134    fn from(addr: grovedb_commitment_tree::PaymentAddress) -> Self {
135        Self(addr)
136    }
137}
138
139/// Infallible conversion from a reference to `PaymentAddress`.
140impl From<&grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
141    fn from(addr: &grovedb_commitment_tree::PaymentAddress) -> Self {
142        Self(*addr)
143    }
144}
145
146impl std::fmt::Display for OrchardAddress {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        let raw = self.to_raw_bytes();
149        write!(
150            f,
151            "Orchard(d={}, pk_d={})",
152            hex::encode(&raw[..ORCHARD_DIVERSIFIER_SIZE]),
153            hex::encode(&raw[ORCHARD_DIVERSIFIER_SIZE..])
154        )
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use bech32::Hrp;
162
163    fn test_orchard_address() -> OrchardAddress {
164        use grovedb_commitment_tree::{FullViewingKey, Scope, SpendingKey};
165        let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
166        let fvk = FullViewingKey::from(&sk);
167        let payment_address = fvk.address_at(0u32, Scope::External);
168        OrchardAddress::from(payment_address)
169    }
170
171    #[test]
172    fn test_orchard_address_raw_bytes_roundtrip() {
173        let address = test_orchard_address();
174        let raw = address.to_raw_bytes();
175        assert_eq!(raw.len(), 43);
176
177        let recovered = OrchardAddress::from_raw_bytes(&raw).unwrap();
178        assert_eq!(recovered, address);
179    }
180
181    #[test]
182    fn test_orchard_bech32m_mainnet_roundtrip() {
183        let address = test_orchard_address();
184
185        let encoded = address.to_bech32m_string(Network::Mainnet);
186        assert!(
187            encoded.starts_with("dash1z"),
188            "Orchard mainnet address should start with 'dash1z', got: {}",
189            encoded
190        );
191
192        let (decoded, network) =
193            OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
194        assert_eq!(decoded, address);
195        assert_eq!(network, Network::Mainnet);
196    }
197
198    #[test]
199    fn test_orchard_bech32m_testnet_roundtrip() {
200        let address = test_orchard_address();
201
202        let encoded = address.to_bech32m_string(Network::Testnet);
203        assert!(
204            encoded.starts_with("tdash1z"),
205            "Orchard testnet address should start with 'tdash1z', got: {}",
206            encoded
207        );
208
209        let (decoded, network) =
210            OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
211        assert_eq!(decoded, address);
212        assert_eq!(network, Network::Testnet);
213    }
214
215    #[test]
216    fn test_orchard_bech32m_wrong_type_byte_fails() {
217        // Manually construct an address with P2PKH type byte (0xb0) but 44-byte payload
218        let hrp = Hrp::parse("dash").unwrap();
219        let mut payload = vec![PlatformAddress::P2PKH_TYPE]; // Wrong type byte
220        payload.extend_from_slice(&[0u8; 43]);
221        let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
222
223        let result = OrchardAddress::from_bech32m_string(&encoded);
224        assert!(result.is_err());
225        assert!(result
226            .unwrap_err()
227            .to_string()
228            .contains("invalid Orchard address type byte"));
229    }
230
231    #[test]
232    fn test_orchard_bech32m_wrong_length_fails() {
233        // Too short (only 20 bytes instead of 43)
234        let hrp = Hrp::parse("dash").unwrap();
235        let mut payload = vec![OrchardAddress::ORCHARD_TYPE];
236        payload.extend_from_slice(&[0u8; 20]);
237        let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
238
239        let result = OrchardAddress::from_bech32m_string(&encoded);
240        assert!(result.is_err());
241        assert!(result
242            .unwrap_err()
243            .to_string()
244            .contains("invalid Orchard address length"));
245    }
246
247    #[test]
248    fn test_orchard_and_platform_addresses_are_distinguishable() {
249        let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
250        let p2sh = PlatformAddress::P2sh([0xAB; 20]);
251        let orchard = test_orchard_address();
252
253        let p2pkh_enc = p2pkh.to_bech32m_string(Network::Mainnet);
254        let p2sh_enc = p2sh.to_bech32m_string(Network::Mainnet);
255        let orchard_enc = orchard.to_bech32m_string(Network::Mainnet);
256
257        // All three start with "dash1" but have different type-byte characters
258        assert!(p2pkh_enc.starts_with("dash1k"), "P2PKH: {}", p2pkh_enc);
259        assert!(p2sh_enc.starts_with("dash1s"), "P2SH: {}", p2sh_enc);
260        assert!(
261            orchard_enc.starts_with("dash1z"),
262            "Orchard: {}",
263            orchard_enc
264        );
265
266        // Cross-decoding should fail
267        assert!(PlatformAddress::from_bech32m_string(&orchard_enc).is_err());
268        assert!(OrchardAddress::from_bech32m_string(&p2pkh_enc).is_err());
269    }
270
271    #[test]
272    fn test_orchard_address_from_raw_bytes_invalid_pk_d() {
273        // All zeros for pk_d is not a valid Pallas curve point
274        let mut raw = [0u8; 43];
275        raw[0] = 0x01; // non-zero diversifier
276        assert!(OrchardAddress::from_raw_bytes(&raw).is_err());
277    }
278
279    #[test]
280    fn test_orchard_address_display() {
281        let address = test_orchard_address();
282        let display = format!("{}", address);
283        assert!(display.starts_with("Orchard(d="));
284        assert!(display.contains("pk_d="));
285    }
286}