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>
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
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));
|
||||
|
|
|
@ -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")]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rpc_getdifficulty() {
|
||||
|
|
Loading…
Reference in New Issue