feat(rpc): Implement the `z_validateaddress` RPC (#6185)

* Add the response type for `z_validateaddress`

* Add UAs to the address variants

* Remove a redundant TODO

* Impl `z_validateaddress`

* Add basic test vectors

* Add basic snapshots

* Remove a deprecated field from the RPC response

* Add basic snapshot data

* Check the semantic validity of UAs

* Refactor imports

* Refactor snapshot filenames

This PR removes the `.new` filename extensions from snapshots.

* Rename `address` to `unified_address`
This commit is contained in:
Marek 2023-02-20 22:06:22 +01:00 committed by GitHub
parent 03929bd205
commit 83d038c067
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 296 additions and 11 deletions

View File

@ -2,12 +2,12 @@
//!
//! Usage: <https://docs.rs/zcash_address/0.2.0/zcash_address/trait.TryFromAddress.html#examples>
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<orchard::Address>,
/// Sapling address
sapling: Option<sapling::PaymentAddress>,
/// Transparent address
transparent: Option<transparent::Address>,
},
}
@ -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<Self, zcash_address::ConversionError<Self::Error>> {
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 {

View File

@ -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<Result<validate_address::Response>>;
/// 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<Result<types::z_validate_address::Response>>;
/// 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<Result<types::z_validate_address::Response>> {
let network = self.network;
async move {
let Ok(address) = raw_address
.parse::<zcash_address::ZcashAddress>() else {
return Ok(z_validate_address::Response::invalid());
};
let address = match address
.convert::<primitives::Address>() {
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<u32>) -> BoxFuture<Result<BlockSubsidy>> {
let latest_chain_tip = self.latest_chain_tip.clone();
let network = self.network;

View File

@ -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;

View File

@ -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<String>,
/// The type of the address.
#[serde(skip_serializing_if = "Option::is_none")]
pub address_type: Option<AddressType>,
/// 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<bool>,
}
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
/// <https://zcash.github.io/rpc/z_validateaddress.html>.
#[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,
}
}
}

View File

@ -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<State, ReadState>(
.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));

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
---
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
expression: z_validate_address
---
{
"isvalid": false
}

View File

@ -0,0 +1,7 @@
---
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
expression: z_validate_address
---
{
"isvalid": false
}

View File

@ -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() {