change(rpc): add validateaddress method (#6086)
* adds validate_address method * Adds snapshot/vectors tests * Checks that the address is transparent * Removes unused pubkey/scriptPubKey fields * adds snapshot for invalid addresses, updates network mismatch log * simplifies is_transparent method * Returns isvalid: false instead of conversion error
This commit is contained in:
parent
dc43dca06f
commit
0793eaf687
|
@ -5488,6 +5488,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"uint",
|
||||
"x25519-dalek",
|
||||
"zcash_address",
|
||||
"zcash_encoding",
|
||||
"zcash_history",
|
||||
"zcash_note_encryption",
|
||||
|
@ -5610,6 +5611,7 @@ dependencies = [
|
|||
"tower",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"zcash_address",
|
||||
"zebra-chain",
|
||||
"zebra-consensus",
|
||||
"zebra-network",
|
||||
|
|
|
@ -13,7 +13,9 @@ default = []
|
|||
# Production features that activate extra functionality
|
||||
|
||||
# Experimental mining RPC support
|
||||
getblocktemplate-rpcs = []
|
||||
getblocktemplate-rpcs = [
|
||||
"zcash_address",
|
||||
]
|
||||
|
||||
# Test-only features
|
||||
|
||||
|
@ -89,6 +91,9 @@ ed25519-zebra = "3.1.0"
|
|||
redjubjub = "0.5.0"
|
||||
reddsa = "0.4.0"
|
||||
|
||||
# Experimental feature getblocktemplate-rpcs
|
||||
zcash_address = { version = "0.2.0", optional = true }
|
||||
|
||||
# Optional testing dependencies
|
||||
proptest = { version = "0.10.1", optional = true }
|
||||
proptest-derive = { version = "0.3.0", optional = true }
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
//! whose functionality is implemented elsewhere.
|
||||
|
||||
mod proofs;
|
||||
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
mod address;
|
||||
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
pub use address::Address;
|
||||
|
||||
pub use ed25519_zebra as ed25519;
|
||||
pub use reddsa;
|
||||
pub use redjubjub;
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
//! `zcash_address` conversion to `zebra_chain` address types.
|
||||
//!
|
||||
//! Usage: <https://docs.rs/zcash_address/0.2.0/zcash_address/trait.TryFromAddress.html#examples>
|
||||
|
||||
use zcash_primitives::sapling;
|
||||
|
||||
use crate::{orchard, parameters::Network, transparent, BoxError};
|
||||
|
||||
/// Zcash address variants
|
||||
// TODO: Add Sprout addresses
|
||||
pub enum Address {
|
||||
/// Transparent address
|
||||
Transparent(transparent::Address),
|
||||
|
||||
/// Sapling address
|
||||
Sapling {
|
||||
/// Address' network
|
||||
network: Network,
|
||||
|
||||
/// Sapling address
|
||||
address: sapling::PaymentAddress,
|
||||
},
|
||||
|
||||
/// Unified address
|
||||
Unified {
|
||||
/// Address' network
|
||||
network: Network,
|
||||
|
||||
/// Transparent address
|
||||
transparent_address: transparent::Address,
|
||||
|
||||
/// Sapling address
|
||||
sapling_address: sapling::PaymentAddress,
|
||||
|
||||
/// Orchard address
|
||||
orchard_address: orchard::Address,
|
||||
},
|
||||
}
|
||||
|
||||
impl TryFrom<zcash_address::Network> for Network {
|
||||
// TODO: better error type
|
||||
type Error = BoxError;
|
||||
|
||||
fn try_from(network: zcash_address::Network) -> Result<Self, Self::Error> {
|
||||
match network {
|
||||
zcash_address::Network::Main => Ok(Network::Mainnet),
|
||||
zcash_address::Network::Test => Ok(Network::Testnet),
|
||||
zcash_address::Network::Regtest => Err("unsupported Zcash network parameters".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Network> for zcash_address::Network {
|
||||
fn from(network: Network) -> Self {
|
||||
match network {
|
||||
Network::Mainnet => zcash_address::Network::Main,
|
||||
Network::Testnet => zcash_address::Network::Test,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl zcash_address::TryFromAddress for Address {
|
||||
// TODO: crate::serialization::SerializationError
|
||||
type Error = BoxError;
|
||||
|
||||
fn try_from_transparent_p2pkh(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 20],
|
||||
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
|
||||
Ok(Self::Transparent(transparent::Address::from_pub_key_hash(
|
||||
network.try_into()?,
|
||||
data,
|
||||
)))
|
||||
}
|
||||
|
||||
fn try_from_transparent_p2sh(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 20],
|
||||
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
|
||||
Ok(Self::Transparent(transparent::Address::from_script_hash(
|
||||
network.try_into()?,
|
||||
data,
|
||||
)))
|
||||
}
|
||||
|
||||
fn try_from_sapling(
|
||||
network: zcash_address::Network,
|
||||
data: [u8; 43],
|
||||
) -> Result<Self, zcash_address::ConversionError<Self::Error>> {
|
||||
let network = network.try_into()?;
|
||||
sapling::PaymentAddress::from_bytes(&data)
|
||||
.map(|address| Self::Sapling { address, network })
|
||||
.ok_or_else(|| BoxError::from("not a valid sapling address").into())
|
||||
}
|
||||
|
||||
// TODO: Add sprout and unified/orchard converters
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Returns the network for the address.
|
||||
pub fn network(&self) -> Network {
|
||||
match &self {
|
||||
Self::Transparent(address) => address.network(),
|
||||
Self::Sapling { network, .. } | Self::Unified { network, .. } => *network,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the address is PayToScriptHash
|
||||
/// Returns false if the address is PayToPublicKeyHash or shielded.
|
||||
pub fn is_script_hash(&self) -> bool {
|
||||
match &self {
|
||||
Self::Transparent(address) => address.is_script_hash(),
|
||||
Self::Sapling { .. } | Self::Unified { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if address is of the [`Address::Transparent`] variant.
|
||||
/// Returns false if otherwise.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
matches!(self, Self::Transparent(_))
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ default = []
|
|||
# Experimental mining RPC support
|
||||
getblocktemplate-rpcs = [
|
||||
"rand",
|
||||
"zcash_address",
|
||||
"zebra-consensus/getblocktemplate-rpcs",
|
||||
"zebra-state/getblocktemplate-rpcs",
|
||||
"zebra-node-services/getblocktemplate-rpcs",
|
||||
|
@ -58,6 +59,8 @@ serde = { version = "1.0.152", features = ["serde_derive"] }
|
|||
|
||||
# Experimental feature getblocktemplate-rpcs
|
||||
rand = { version = "0.8.5", package = "rand", optional = true }
|
||||
# ECC deps used by getblocktemplate-rpcs feature
|
||||
zcash_address = { version = "0.2.0", optional = true }
|
||||
|
||||
# Test-only feature proptest-impl
|
||||
proptest = { version = "0.10.1", optional = true }
|
||||
|
|
|
@ -7,12 +7,15 @@ use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
|
|||
use jsonrpc_derive::rpc;
|
||||
use tower::{buffer::Buffer, Service, ServiceExt};
|
||||
|
||||
use zcash_address;
|
||||
|
||||
use zebra_chain::{
|
||||
amount::Amount,
|
||||
block::{self, Block, Height},
|
||||
chain_sync_status::ChainSyncStatus,
|
||||
chain_tip::ChainTip,
|
||||
parameters::Network,
|
||||
primitives,
|
||||
serialization::ZcashDeserializeInto,
|
||||
transparent,
|
||||
};
|
||||
|
@ -43,6 +46,7 @@ use crate::methods::{
|
|||
peer_info::PeerInfo,
|
||||
submit_block,
|
||||
subsidy::{BlockSubsidy, FundingStream},
|
||||
validate_address,
|
||||
},
|
||||
},
|
||||
height_from_signed_int, GetBlockHash, MISSING_BLOCK_ERROR_CODE,
|
||||
|
@ -167,6 +171,13 @@ pub trait GetBlockTemplateRpc {
|
|||
#[rpc(name = "getpeerinfo")]
|
||||
fn get_peer_info(&self) -> BoxFuture<Result<Vec<PeerInfo>>>;
|
||||
|
||||
/// Checks if a zcash address is valid.
|
||||
/// Returns information about the given address if valid.
|
||||
///
|
||||
/// zcashd reference: [`validateaddress`](https://zcash.github.io/rpc/validateaddress.html)
|
||||
#[rpc(name = "validateaddress")]
|
||||
fn validate_address(&self, address: String) -> BoxFuture<Result<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.
|
||||
///
|
||||
|
@ -788,6 +799,51 @@ where
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn validate_address(
|
||||
&self,
|
||||
raw_address: String,
|
||||
) -> BoxFuture<Result<validate_address::Response>> {
|
||||
let network = self.network;
|
||||
|
||||
async move {
|
||||
let Ok(address) = raw_address
|
||||
.parse::<zcash_address::ZcashAddress>() else {
|
||||
return Ok(validate_address::Response::invalid());
|
||||
};
|
||||
|
||||
let address = match address
|
||||
.convert::<primitives::Address>() {
|
||||
Ok(address) => address,
|
||||
Err(err) => {
|
||||
tracing::debug!(?err, "conversion error");
|
||||
return Ok(validate_address::Response::invalid());
|
||||
}
|
||||
};
|
||||
|
||||
// we want to match zcashd's behaviour
|
||||
if !address.is_transparent() {
|
||||
return Ok(validate_address::Response::invalid());
|
||||
}
|
||||
|
||||
return Ok(if address.network() == network {
|
||||
validate_address::Response {
|
||||
address: Some(raw_address),
|
||||
is_valid: true,
|
||||
is_script: Some(address.is_script_hash()),
|
||||
}
|
||||
} else {
|
||||
tracing::info!(
|
||||
?network,
|
||||
address_network = ?address.network(),
|
||||
"invalid address in validateaddress RPC: Zebra's configured network must match address network"
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
|
@ -9,4 +9,5 @@ pub mod peer_info;
|
|||
pub mod submit_block;
|
||||
pub mod subsidy;
|
||||
pub mod transaction;
|
||||
pub mod validate_address;
|
||||
pub mod zec;
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
//! for the `submitblock` RPC method.
|
||||
|
||||
/// Deserialize hex-encoded strings to bytes.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HexData(#[serde(with = "hex")] pub Vec<u8>);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//! Response type for the `validateaddress` RPC.
|
||||
|
||||
/// `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>,
|
||||
|
||||
/// If the key is a script.
|
||||
#[serde(rename = "isscript")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_script: Option<bool>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates an empty response with `isvalid` of false.
|
||||
pub fn invalid() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ use crate::methods::{
|
|||
peer_info::PeerInfo,
|
||||
submit_block,
|
||||
subsidy::BlockSubsidy,
|
||||
validate_address,
|
||||
},
|
||||
},
|
||||
tests::utils::fake_history_tree,
|
||||
|
@ -371,6 +372,24 @@ pub async fn test_responses<State, ReadState>(
|
|||
.expect("unexpected error in submitblock RPC call");
|
||||
|
||||
snapshot_rpc_submit_block_invalid(submit_block, &settings);
|
||||
|
||||
// `validateaddress`
|
||||
let founder_address = match network {
|
||||
Network::Mainnet => "t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR",
|
||||
Network::Testnet => "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi",
|
||||
};
|
||||
|
||||
let validate_address = get_block_template_rpc
|
||||
.validate_address(founder_address.to_string())
|
||||
.await
|
||||
.expect("We should have a validate_address::Response");
|
||||
snapshot_rpc_validateaddress("basic", validate_address, &settings);
|
||||
|
||||
let validate_address = get_block_template_rpc
|
||||
.validate_address("".to_string())
|
||||
.await
|
||||
.expect("We should have a validate_address::Response");
|
||||
snapshot_rpc_validateaddress("invalid", validate_address, &settings);
|
||||
}
|
||||
|
||||
/// Snapshot `getblockcount` response, using `cargo insta` and JSON serialization.
|
||||
|
@ -436,3 +455,14 @@ fn snapshot_rpc_getpeerinfo(get_peer_info: Vec<PeerInfo>, settings: &insta::Sett
|
|||
fn snapshot_rpc_getnetworksolps(get_network_sol_ps: u64, settings: &insta::Settings) {
|
||||
settings.bind(|| insta::assert_json_snapshot!("get_network_sol_ps", get_network_sol_ps));
|
||||
}
|
||||
|
||||
/// Snapshot `validateaddress` response, using `cargo insta` and JSON serialization.
|
||||
fn snapshot_rpc_validateaddress(
|
||||
variant: &'static str,
|
||||
validate_address: validate_address::Response,
|
||||
settings: &insta::Settings,
|
||||
) {
|
||||
settings.bind(|| {
|
||||
insta::assert_json_snapshot!(format!("validate_address_{variant}"), validate_address)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||
expression: validate_address
|
||||
---
|
||||
{
|
||||
"isvalid": true,
|
||||
"address": "t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR",
|
||||
"isscript": true
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||
expression: validate_address
|
||||
---
|
||||
{
|
||||
"isvalid": true,
|
||||
"address": "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi",
|
||||
"isscript": true
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||
expression: validate_address
|
||||
---
|
||||
{
|
||||
"isvalid": false
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
|
||||
expression: validate_address
|
||||
---
|
||||
{
|
||||
"isvalid": false
|
||||
}
|
|
@ -1290,3 +1290,48 @@ async fn rpc_submitblock_errors() {
|
|||
|
||||
// See zebrad::tests::acceptance::submit_block for success case.
|
||||
}
|
||||
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn rpc_validateaddress() {
|
||||
use get_block_template_rpcs::types::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 validate_address = get_block_template_rpc
|
||||
.validate_address("t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR".to_string())
|
||||
.await
|
||||
.expect("we should have a validate_address::Response");
|
||||
|
||||
assert!(
|
||||
validate_address.is_valid,
|
||||
"Mainnet founder address should be valid on Mainnet"
|
||||
);
|
||||
|
||||
let validate_address = get_block_template_rpc
|
||||
.validate_address("t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi".to_string())
|
||||
.await
|
||||
.expect("We should have a validate_address::Response");
|
||||
|
||||
assert_eq!(
|
||||
validate_address,
|
||||
validate_address::Response::invalid(),
|
||||
"Testnet founder address should be invalid on Mainnet"
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue