Skip to main content

dpp/address_funds/
orchard_address.rs

1use bech32::{Bech32m, Hrp};
2use dashcore::Network;
3
4use crate::address_funds::platform_address::classify_platform_hrp;
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    /// Accepts both `dash` (mainnet) and `tdash` (non-mainnet) HRPs.
91    /// The address is network-agnostic; callers that need a network guard should
92    /// use [`PlatformAddress::is_mainnet_bech32m`] before decoding.
93    ///
94    /// # Returns
95    /// - `Ok(OrchardAddress)` - The decoded address
96    /// - `Err(ProtocolError)` - If the string is malformed or its HRP is not a
97    ///   recognized platform HRP
98    pub fn from_bech32m_string(s: &str) -> Result<Self, ProtocolError> {
99        let (hrp, data) =
100            bech32::decode(s).map_err(|e| ProtocolError::DecodingError(format!("{}", e)))?;
101
102        classify_platform_hrp(&hrp.as_str().to_ascii_lowercase())?;
103
104        // Validate payload: 1 type byte + 11 diversifier + 32 pk_d = 44 bytes
105        if data.len() != 1 + ORCHARD_ADDRESS_SIZE {
106            return Err(ProtocolError::DecodingError(format!(
107                "invalid Orchard address length: expected {} bytes, got {}",
108                1 + ORCHARD_ADDRESS_SIZE,
109                data.len()
110            )));
111        }
112
113        if data[0] != Self::ORCHARD_TYPE {
114            return Err(ProtocolError::DecodingError(format!(
115                "invalid Orchard address type byte: expected 0x{:02x}, got 0x{:02x}",
116                Self::ORCHARD_TYPE,
117                data[0]
118            )));
119        }
120
121        let mut raw = [0u8; ORCHARD_ADDRESS_SIZE];
122        raw.copy_from_slice(&data[1..]);
123        Self::from_raw_bytes(&raw)
124    }
125}
126
127/// Infallible conversion from the orchard crate's `PaymentAddress` to `OrchardAddress`.
128impl From<grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
129    fn from(addr: grovedb_commitment_tree::PaymentAddress) -> Self {
130        Self(addr)
131    }
132}
133
134/// Infallible conversion from a reference to `PaymentAddress`.
135impl From<&grovedb_commitment_tree::PaymentAddress> for OrchardAddress {
136    fn from(addr: &grovedb_commitment_tree::PaymentAddress) -> Self {
137        Self(*addr)
138    }
139}
140
141impl std::fmt::Display for OrchardAddress {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        let raw = self.to_raw_bytes();
144        write!(
145            f,
146            "Orchard(d={}, pk_d={})",
147            hex::encode(&raw[..ORCHARD_DIVERSIFIER_SIZE]),
148            hex::encode(&raw[ORCHARD_DIVERSIFIER_SIZE..])
149        )
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use bech32::Hrp;
157
158    fn test_orchard_address() -> OrchardAddress {
159        use grovedb_commitment_tree::{FullViewingKey, Scope, SpendingKey};
160        let sk = SpendingKey::from_bytes([42u8; 32]).unwrap();
161        let fvk = FullViewingKey::from(&sk);
162        let payment_address = fvk.address_at(0u32, Scope::External);
163        OrchardAddress::from(payment_address)
164    }
165
166    #[test]
167    fn test_orchard_address_raw_bytes_roundtrip() {
168        let address = test_orchard_address();
169        let raw = address.to_raw_bytes();
170        assert_eq!(raw.len(), 43);
171
172        let recovered = OrchardAddress::from_raw_bytes(&raw).unwrap();
173        assert_eq!(recovered, address);
174    }
175
176    #[test]
177    fn test_orchard_bech32m_mainnet_roundtrip() {
178        let address = test_orchard_address();
179
180        let encoded = address.to_bech32m_string(Network::Mainnet);
181        assert!(
182            encoded.starts_with("dash1z"),
183            "Orchard mainnet address should start with 'dash1z', got: {}",
184            encoded
185        );
186
187        let decoded =
188            OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
189        assert_eq!(decoded, address);
190    }
191
192    #[test]
193    fn test_orchard_bech32m_testnet_roundtrip() {
194        let address = test_orchard_address();
195
196        let encoded = address.to_bech32m_string(Network::Testnet);
197        assert!(
198            encoded.starts_with("tdash1z"),
199            "Orchard testnet address should start with 'tdash1z', got: {}",
200            encoded
201        );
202
203        let decoded =
204            OrchardAddress::from_bech32m_string(&encoded).expect("decoding should succeed");
205        assert_eq!(decoded, address);
206    }
207
208    #[test]
209    fn test_orchard_bech32m_wrong_type_byte_fails() {
210        // Manually construct an address with P2PKH type byte (0xb0) but 44-byte payload
211        let hrp = Hrp::parse("dash").unwrap();
212        let mut payload = vec![PlatformAddress::P2PKH_TYPE]; // Wrong type byte
213        payload.extend_from_slice(&[0u8; 43]);
214        let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
215
216        let result = OrchardAddress::from_bech32m_string(&encoded);
217        assert!(result.is_err());
218        assert!(result
219            .unwrap_err()
220            .to_string()
221            .contains("invalid Orchard address type byte"));
222    }
223
224    #[test]
225    fn test_orchard_bech32m_wrong_length_fails() {
226        // Too short (only 20 bytes instead of 43)
227        let hrp = Hrp::parse("dash").unwrap();
228        let mut payload = vec![OrchardAddress::ORCHARD_TYPE];
229        payload.extend_from_slice(&[0u8; 20]);
230        let encoded = bech32::encode::<Bech32m>(hrp, &payload).unwrap();
231
232        let result = OrchardAddress::from_bech32m_string(&encoded);
233        assert!(result.is_err());
234        assert!(result
235            .unwrap_err()
236            .to_string()
237            .contains("invalid Orchard address length"));
238    }
239
240    #[test]
241    fn test_orchard_and_platform_addresses_are_distinguishable() {
242        let p2pkh = PlatformAddress::P2pkh([0xAB; 20]);
243        let p2sh = PlatformAddress::P2sh([0xAB; 20]);
244        let orchard = test_orchard_address();
245
246        let p2pkh_enc = p2pkh.to_bech32m_string(Network::Mainnet);
247        let p2sh_enc = p2sh.to_bech32m_string(Network::Mainnet);
248        let orchard_enc = orchard.to_bech32m_string(Network::Mainnet);
249
250        // All three start with "dash1" but have different type-byte characters
251        assert!(p2pkh_enc.starts_with("dash1k"), "P2PKH: {}", p2pkh_enc);
252        assert!(p2sh_enc.starts_with("dash1s"), "P2SH: {}", p2sh_enc);
253        assert!(
254            orchard_enc.starts_with("dash1z"),
255            "Orchard: {}",
256            orchard_enc
257        );
258
259        // Cross-decoding should fail
260        assert!(PlatformAddress::from_bech32m_string(&orchard_enc).is_err());
261        assert!(OrchardAddress::from_bech32m_string(&p2pkh_enc).is_err());
262    }
263
264    #[test]
265    fn test_orchard_address_from_raw_bytes_invalid_pk_d() {
266        // All zeros for pk_d is not a valid Pallas curve point
267        let mut raw = [0u8; 43];
268        raw[0] = 0x01; // non-zero diversifier
269        assert!(OrchardAddress::from_raw_bytes(&raw).is_err());
270    }
271
272    #[test]
273    fn test_orchard_address_display() {
274        let address = test_orchard_address();
275        let display = format!("{}", address);
276        assert!(display.starts_with("Orchard(d="));
277        assert!(display.contains("pk_d="));
278    }
279}