diff --git a/zebra-chain/src/primitives/address.rs b/zebra-chain/src/primitives/address.rs index 8753f856..be0b8587 100644 --- a/zebra-chain/src/primitives/address.rs +++ b/zebra-chain/src/primitives/address.rs @@ -2,12 +2,12 @@ //! //! Usage: +use zcash_address::unified::{self, Container}; use zcash_primitives::sapling; -use crate::{orchard, parameters::Network, transparent, BoxError}; +use crate::{parameters::Network, transparent, BoxError}; /// Zcash address variants -// TODO: Add Sprout addresses pub enum Address { /// Transparent address Transparent(transparent::Address), @@ -26,14 +26,17 @@ pub enum Address { /// Address' network network: Network, - /// Transparent address - transparent_address: transparent::Address, - - /// Sapling address - sapling_address: sapling::PaymentAddress, + /// Unified address + unified_address: zcash_address::unified::Address, /// Orchard address - orchard_address: orchard::Address, + orchard: Option, + + /// Sapling address + sapling: Option, + + /// Transparent address + transparent: Option, }, } @@ -93,7 +96,61 @@ impl zcash_address::TryFromAddress for Address { .ok_or_else(|| BoxError::from("not a valid sapling address").into()) } - // TODO: Add sprout and unified/orchard converters + fn try_from_unified( + network: zcash_address::Network, + unified_address: zcash_address::unified::Address, + ) -> Result> { + let network = network.try_into()?; + let mut orchard = None; + let mut sapling = None; + let mut transparent = None; + + for receiver in unified_address.items().into_iter() { + match receiver { + unified::Receiver::Orchard(data) => { + orchard = orchard::Address::from_raw_address_bytes(&data).into(); + // ZIP 316: Consumers MUST reject Unified Addresses/Viewing Keys in + // which any constituent Item does not meet the validation + // requirements of its encoding. + if orchard.is_none() { + return Err(BoxError::from( + "Unified Address contains an invalid Orchard receiver.", + ) + .into()); + } + } + unified::Receiver::Sapling(data) => { + sapling = sapling::PaymentAddress::from_bytes(&data); + // ZIP 316: Consumers MUST reject Unified Addresses/Viewing Keys in + // which any constituent Item does not meet the validation + // requirements of its encoding. + if sapling.is_none() { + return Err(BoxError::from( + "Unified Address contains an invalid Sapling receiver", + ) + .into()); + } + } + unified::Receiver::P2pkh(data) => { + transparent = Some(transparent::Address::from_pub_key_hash(network, data)); + } + unified::Receiver::P2sh(data) => { + transparent = Some(transparent::Address::from_script_hash(network, data)); + } + unified::Receiver::Unknown { .. } => { + return Err(BoxError::from("Unsupported receiver in a Unified Address.").into()); + } + } + } + + Ok(Self::Unified { + network, + unified_address, + orchard, + sapling, + transparent, + }) + } } impl Address { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 2af2bca7..1e2e0379 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -47,7 +47,7 @@ use crate::methods::{ peer_info::PeerInfo, submit_block, subsidy::{BlockSubsidy, FundingStream}, - unified_address, validate_address, + unified_address, validate_address, z_validate_address, }, }, height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE, @@ -179,6 +179,16 @@ pub trait GetBlockTemplateRpc { #[rpc(name = "validateaddress")] fn validate_address(&self, address: String) -> BoxFuture>; + /// Checks if a zcash address is valid. + /// Returns information about the given address if valid. + /// + /// zcashd reference: [`z_validateaddress`](https://zcash.github.io/rpc/z_validateaddress.html) + #[rpc(name = "z_validateaddress")] + fn z_validate_address( + &self, + address: String, + ) -> BoxFuture>; + /// Returns the block subsidy reward of the block at `height`, taking into account the mining slow start. /// Returns an error if `height` is less than the height of the first halving for the current network. /// @@ -860,6 +870,49 @@ where .boxed() } + fn z_validate_address( + &self, + raw_address: String, + ) -> BoxFuture> { + let network = self.network; + + async move { + let Ok(address) = raw_address + .parse::() else { + return Ok(z_validate_address::Response::invalid()); + }; + + let address = match address + .convert::() { + Ok(address) => address, + Err(err) => { + tracing::debug!(?err, "conversion error"); + return Ok(z_validate_address::Response::invalid()); + } + }; + + if address.network() == network { + Ok(z_validate_address::Response { + is_valid: true, + address: Some(raw_address), + address_type: Some(z_validate_address::AddressType::from(&address)), + is_mine: Some(false), + }) + } else { + tracing::info!( + ?network, + address_network = ?address.network(), + "invalid address network in z_validateaddress RPC: address is for {:?} but Zebra is on {:?}", + address.network(), + network + ); + + Ok(z_validate_address::Response::invalid()) + } + } + .boxed() + } + fn get_block_subsidy(&self, height: Option) -> BoxFuture> { let latest_chain_tip = self.latest_chain_tip.clone(); let network = self.network; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs index c52c196a..fc3b94ce 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs @@ -11,4 +11,5 @@ pub mod subsidy; pub mod transaction; pub mod unified_address; pub mod validate_address; +pub mod z_validate_address; pub mod zec; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/z_validate_address.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/z_validate_address.rs new file mode 100644 index 00000000..43ed1cc5 --- /dev/null +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/z_validate_address.rs @@ -0,0 +1,66 @@ +//! Response type for the `z_validateaddress` RPC. + +use zebra_chain::primitives::Address; + +/// `z_validateaddress` response +#[derive(Clone, Default, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Response { + /// Whether the address is valid. + /// + /// If not, this is the only property returned. + #[serde(rename = "isvalid")] + pub is_valid: bool, + + /// The zcash address that has been validated. + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + + /// The type of the address. + #[serde(skip_serializing_if = "Option::is_none")] + pub address_type: Option, + + /// Whether the address is yours or not. + /// + /// Always false for now since Zebra doesn't have a wallet yet. + #[serde(rename = "ismine")] + #[serde(skip_serializing_if = "Option::is_none")] + pub is_mine: Option, +} + +impl Response { + /// Creates an empty response with `isvalid` of false. + pub fn invalid() -> Self { + Self::default() + } +} + +/// Address types supported by the `z_validateaddress` RPC according to +/// . +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AddressType { + /// The `p2pkh` address type. + P2pkh, + /// The `p2sh` address type. + P2sh, + /// The `sapling` address type. + Sapling, + /// The `unified` address type. + Unified, +} + +impl From<&Address> for AddressType { + fn from(address: &Address) -> Self { + match address { + Address::Transparent(_) => { + if address.is_script_hash() { + Self::P2sh + } else { + Self::P2pkh + } + } + Address::Sapling { .. } => Self::Sapling, + Address::Unified { .. } => Self::Unified, + } + } +} diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 1b0035b4..3d0e718a 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -42,7 +42,7 @@ use crate::methods::{ peer_info::PeerInfo, submit_block, subsidy::BlockSubsidy, - unified_address, validate_address, + unified_address, validate_address, z_validate_address, }, }, tests::utils::fake_history_tree, @@ -391,6 +391,24 @@ pub async fn test_responses( .expect("We should have a validate_address::Response"); snapshot_rpc_validateaddress("invalid", validate_address, &settings); + // `z_validateaddress` + let founder_address = match network { + Network::Mainnet => "t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR", + Network::Testnet => "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", + }; + + let z_validate_address = get_block_template_rpc + .z_validate_address(founder_address.to_string()) + .await + .expect("We should have a z_validate_address::Response"); + snapshot_rpc_z_validateaddress("basic", z_validate_address, &settings); + + let z_validate_address = get_block_template_rpc + .z_validate_address("".to_string()) + .await + .expect("We should have a z_validate_address::Response"); + snapshot_rpc_z_validateaddress("invalid", z_validate_address, &settings); + // getdifficulty // Fake the ChainInfo response @@ -498,6 +516,17 @@ fn snapshot_rpc_validateaddress( }); } +/// Snapshot `z_validateaddress` response, using `cargo insta` and JSON serialization. +fn snapshot_rpc_z_validateaddress( + variant: &'static str, + z_validate_address: z_validate_address::Response, + settings: &insta::Settings, +) { + settings.bind(|| { + insta::assert_json_snapshot!(format!("z_validate_address_{variant}"), z_validate_address) + }); +} + /// Snapshot `getdifficulty` response, using `cargo insta` and JSON serialization. fn snapshot_rpc_getdifficulty(difficulty: f64, settings: &insta::Settings) { settings.bind(|| insta::assert_json_snapshot!("get_difficulty", difficulty)); diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@mainnet_10.snap new file mode 100644 index 00000000..97e32cb7 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@mainnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: z_validate_address +--- +{ + "isvalid": true, + "address": "t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR", + "address_type": "p2sh", + "ismine": false +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@testnet_10.snap new file mode 100644 index 00000000..cc2ec359 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_basic@testnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: z_validate_address +--- +{ + "isvalid": true, + "address": "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi", + "address_type": "p2sh", + "ismine": false +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@mainnet_10.snap new file mode 100644 index 00000000..2785e300 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@mainnet_10.snap @@ -0,0 +1,7 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: z_validate_address +--- +{ + "isvalid": false +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@testnet_10.snap new file mode 100644 index 00000000..2785e300 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/z_validate_address_invalid@testnet_10.snap @@ -0,0 +1,7 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: z_validate_address +--- +{ + "isvalid": false +} diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index e3a9f8aa..c7cd54a1 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -1434,6 +1434,51 @@ async fn rpc_validateaddress() { ); } +#[cfg(feature = "getblocktemplate-rpcs")] +#[tokio::test(flavor = "multi_thread")] +async fn rpc_z_validateaddress() { + use get_block_template_rpcs::types::z_validate_address; + use zebra_chain::{chain_sync_status::MockSyncStatus, chain_tip::mock::MockChainTip}; + use zebra_network::address_book_peers::MockAddressBookPeers; + + let _init_guard = zebra_test::init(); + + let (mock_chain_tip, _mock_chain_tip_sender) = MockChainTip::new(); + + // Init RPC + let get_block_template_rpc = get_block_template_rpcs::GetBlockTemplateRpcImpl::new( + Mainnet, + Default::default(), + Buffer::new(MockService::build().for_unit_tests(), 1), + MockService::build().for_unit_tests(), + mock_chain_tip, + MockService::build().for_unit_tests(), + MockSyncStatus::default(), + MockAddressBookPeers::default(), + ); + + let z_validate_address = get_block_template_rpc + .z_validate_address("t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR".to_string()) + .await + .expect("we should have a z_validate_address::Response"); + + assert!( + z_validate_address.is_valid, + "Mainnet founder address should be valid on Mainnet" + ); + + let z_validate_address = get_block_template_rpc + .z_validate_address("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi".to_string()) + .await + .expect("We should have a z_validate_address::Response"); + + assert_eq!( + z_validate_address, + z_validate_address::Response::invalid(), + "Testnet founder address should be invalid on Mainnet" + ); +} + #[cfg(feature = "getblocktemplate-rpcs")] #[tokio::test(flavor = "multi_thread")] async fn rpc_getdifficulty() {