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:
parent
03929bd205
commit
83d038c067
|
@ -2,12 +2,12 @@
|
||||||
//!
|
//!
|
||||||
//! Usage: <https://docs.rs/zcash_address/0.2.0/zcash_address/trait.TryFromAddress.html#examples>
|
//! 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 zcash_primitives::sapling;
|
||||||
|
|
||||||
use crate::{orchard, parameters::Network, transparent, BoxError};
|
use crate::{parameters::Network, transparent, BoxError};
|
||||||
|
|
||||||
/// Zcash address variants
|
/// Zcash address variants
|
||||||
// TODO: Add Sprout addresses
|
|
||||||
pub enum Address {
|
pub enum Address {
|
||||||
/// Transparent address
|
/// Transparent address
|
||||||
Transparent(transparent::Address),
|
Transparent(transparent::Address),
|
||||||
|
@ -26,14 +26,17 @@ pub enum Address {
|
||||||
/// Address' network
|
/// Address' network
|
||||||
network: Network,
|
network: Network,
|
||||||
|
|
||||||
/// Transparent address
|
/// Unified address
|
||||||
transparent_address: transparent::Address,
|
unified_address: zcash_address::unified::Address,
|
||||||
|
|
||||||
/// Sapling address
|
|
||||||
sapling_address: sapling::PaymentAddress,
|
|
||||||
|
|
||||||
/// Orchard 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())
|
.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 {
|
impl Address {
|
||||||
|
|
|
@ -47,7 +47,7 @@ use crate::methods::{
|
||||||
peer_info::PeerInfo,
|
peer_info::PeerInfo,
|
||||||
submit_block,
|
submit_block,
|
||||||
subsidy::{BlockSubsidy, FundingStream},
|
subsidy::{BlockSubsidy, FundingStream},
|
||||||
unified_address, validate_address,
|
unified_address, validate_address, z_validate_address,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
||||||
|
@ -179,6 +179,16 @@ pub trait GetBlockTemplateRpc {
|
||||||
#[rpc(name = "validateaddress")]
|
#[rpc(name = "validateaddress")]
|
||||||
fn validate_address(&self, address: String) -> BoxFuture<Result<validate_address::Response>>;
|
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 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.
|
/// Returns an error if `height` is less than the height of the first halving for the current network.
|
||||||
///
|
///
|
||||||
|
@ -860,6 +870,49 @@ where
|
||||||
.boxed()
|
.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>> {
|
fn get_block_subsidy(&self, height: Option<u32>) -> BoxFuture<Result<BlockSubsidy>> {
|
||||||
let latest_chain_tip = self.latest_chain_tip.clone();
|
let latest_chain_tip = self.latest_chain_tip.clone();
|
||||||
let network = self.network;
|
let network = self.network;
|
||||||
|
|
|
@ -11,4 +11,5 @@ pub mod subsidy;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
pub mod unified_address;
|
pub mod unified_address;
|
||||||
pub mod validate_address;
|
pub mod validate_address;
|
||||||
|
pub mod z_validate_address;
|
||||||
pub mod zec;
|
pub mod zec;
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ use crate::methods::{
|
||||||
peer_info::PeerInfo,
|
peer_info::PeerInfo,
|
||||||
submit_block,
|
submit_block,
|
||||||
subsidy::BlockSubsidy,
|
subsidy::BlockSubsidy,
|
||||||
unified_address, validate_address,
|
unified_address, validate_address, z_validate_address,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tests::utils::fake_history_tree,
|
tests::utils::fake_history_tree,
|
||||||
|
@ -391,6 +391,24 @@ pub async fn test_responses<State, ReadState>(
|
||||||
.expect("We should have a validate_address::Response");
|
.expect("We should have a validate_address::Response");
|
||||||
snapshot_rpc_validateaddress("invalid", validate_address, &settings);
|
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
|
// getdifficulty
|
||||||
|
|
||||||
// Fake the ChainInfo response
|
// 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.
|
/// Snapshot `getdifficulty` response, using `cargo insta` and JSON serialization.
|
||||||
fn snapshot_rpc_getdifficulty(difficulty: f64, settings: &insta::Settings) {
|
fn snapshot_rpc_getdifficulty(difficulty: f64, settings: &insta::Settings) {
|
||||||
settings.bind(|| insta::assert_json_snapshot!("get_difficulty", difficulty));
|
settings.bind(|| insta::assert_json_snapshot!("get_difficulty", difficulty));
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||||
|
expression: z_validate_address
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"isvalid": false
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||||
|
expression: z_validate_address
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"isvalid": false
|
||||||
|
}
|
|
@ -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")]
|
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn rpc_getdifficulty() {
|
async fn rpc_getdifficulty() {
|
||||||
|
|
Loading…
Reference in New Issue