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:
Arya 2023-02-02 21:26:58 -05:00 committed by GitHub
parent dc43dca06f
commit 0793eaf687
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 332 additions and 2 deletions

2
Cargo.lock generated
View File

@ -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",

View File

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

View File

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

View File

@ -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(_))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
);
}