fix(rpc): Refactor `getrawtransaction` & RPC error handling (#9049)

* clean-up: simplify the def of `MapServerError`

* Use `HexData` instead of `String` for TXIDs

* Remove a redundant test

We don't need such a test anymore because the deserialization is handled
by Serde now.

* Adjust tests for using `HexData`

* Make `height` and `confirmations` optional

* Use legacy error codes

* fmt

* Remove unneeded error codes

* Remove `zebra-rpc/src/constants.rs`

* Rename `MapServerError` to `MapError`

* Rename `OkOrServerError` to `OkOrError`

* Allow specifying error codes when mapping errors

* Allow setting error codes when mapping options

* Use the right error code for `getrawtransaction`

* fmt

* Add docs for the error conversion traits

* Refactor the error handling for `getblock`

* Refactor error handling in `sendrawtransaction`

* Refactor the error handling for `getblock`

* Update the error handling for `getrawtransaction`

* Refactor error handling for `z_gettreestate`

* Refactor the error handling for address parsing

* Refactor the error handling for getrawtransaction

* Update `z_gettreestate` snapshots

* Cosmetics

* Refactor error handling in `getblock`

* Refactor error handling in `getblockheader`

* Simplify `getrawtransaction`

* Check errors for `getrawtransaction`

* fmt

* Simplify proptests

* Fix unit tests for `getaddresstxids`

* Fix unit tests for `getaddressutxos`

* fix docs

* Update snapshots for `getrawtransaction`

* Update zebra-rpc/src/server/error.rs

Co-authored-by: Arya <aryasolhi@gmail.com>

* Use `transaction::Hash` instead of `HexData`

* Simplify error handling

* Update zebra-rpc/src/server/error.rs

Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>

* Move a note on performance

* Fix a typo

* Use `String` instead of `transaction::Hash`

* Adjust and add proptests

* Reintroduce snapshots for invalid TXIDs

* Don't derive `Serialize` & `Deserialize` for txids

Deriving `serde::Serialize` & `serde::Deserialize` for
`transaction::Hash` was superfluous, and we didn't need it anywhere in
the code.

---------

Co-authored-by: Arya <aryasolhi@gmail.com>
Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Marek 2024-12-13 15:01:53 +01:00 committed by GitHub
parent 568b25e590
commit b0c4d19a7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 764 additions and 877 deletions

View File

@ -34,7 +34,6 @@ use std::{fmt, sync::Arc};
use proptest_derive::Arbitrary;
use hex::{FromHex, ToHex};
use serde::{Deserialize, Serialize};
use crate::serialization::{
ReadZcashExt, SerializationError, WriteZcashExt, ZcashDeserialize, ZcashSerialize,
@ -56,7 +55,7 @@ use super::{txid::TxIdBuilder, AuthDigest, Transaction};
///
/// [ZIP-244]: https://zips.z.cash/zip-0244
/// [Spec: Transaction Identifiers]: https://zips.z.cash/protocol/protocol.pdf#txnidentifiers
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
pub struct Hash(pub [u8; 32]);

View File

@ -1,44 +0,0 @@
//! Constants for RPC methods and server responses.
use jsonrpc_core::{Error, ErrorCode};
/// The RPC error code used by `zcashd` for incorrect RPC parameters.
///
/// [`jsonrpc_core`] uses these codes:
/// <https://github.com/paritytech/jsonrpc/blob/609d7a6cc160742d035510fa89fb424ccf077660/core/src/types/error.rs#L25-L36>
///
/// `node-stratum-pool` mining pool library expects error code `-1` to detect available RPC methods:
/// <https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L459>
pub const INVALID_PARAMETERS_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-1);
/// The RPC error code used by `zcashd` for missing blocks, when looked up
/// by hash.
pub const INVALID_ADDRESS_OR_KEY_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-5);
/// The RPC error code used by `zcashd` for missing blocks.
///
/// `lightwalletd` expects error code `-8` when a block is not found:
/// <https://github.com/zcash/lightwalletd/blob/v0.4.16/common/common.go#L287-L290>
pub const MISSING_BLOCK_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-8);
/// The RPC error code used by `zcashd` when there are no blocks in the state.
///
/// `lightwalletd` expects error code `0` when there are no blocks in the state.
//
// TODO: find the source code that expects or generates this error
pub const NO_BLOCKS_IN_STATE_ERROR_CODE: ErrorCode = ErrorCode::ServerError(0);
/// The RPC error used by `zcashd` when there are no blocks in the state.
//
// TODO: find the source code that expects or generates this error text, if there is any
// replace literal Error { ... } with this error
pub fn no_blocks_in_state_error() -> Error {
Error {
code: NO_BLOCKS_IN_STATE_ERROR_CODE,
message: "No blocks in state".to_string(),
data: None,
}
}
/// When logging parameter data, only log this much data.
pub const MAX_PARAMS_LOG_LENGTH: usize = 100;

View File

@ -5,7 +5,6 @@
#![doc(html_root_url = "https://docs.rs/zebra_rpc")]
pub mod config;
pub mod constants;
pub mod methods;
pub mod queue;
pub mod server;

View File

@ -6,7 +6,7 @@
//! Some parts of the `zcashd` RPC documentation are outdated.
//! So this implementation follows the `zcashd` server and `lightwalletd` client implementations.
use std::{collections::HashSet, fmt::Debug, sync::Arc};
use std::{collections::HashSet, fmt::Debug};
use chrono::Utc;
use futures::{stream::FuturesOrdered, FutureExt, StreamExt, TryFutureExt};
@ -34,21 +34,19 @@ use zebra_chain::{
},
};
use zebra_node_services::mempool;
use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation};
use zebra_state::{HashOrHeight, OutputIndex, OutputLocation, TransactionLocation};
use crate::{
constants::{
INVALID_ADDRESS_OR_KEY_ERROR_CODE, INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE,
},
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
queue::Queue,
server::{
self,
error::{MapError, OkOrError},
},
};
mod errors;
pub mod hex_data;
use errors::{MapServerError, OkOrServerError};
// We don't use a types/ module here, because it is redundant.
pub mod trees;
@ -291,7 +289,7 @@ pub trait Rpc {
#[rpc(name = "getrawtransaction")]
fn get_raw_transaction(
&self,
txid_hex: String,
txid: String,
verbose: Option<u8>,
) -> BoxFuture<Result<GetRawTransaction>>;
@ -564,7 +562,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let zebra_state::ReadResponse::TipPoolValues {
tip_height,
@ -580,7 +578,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let zebra_state::ReadResponse::BlockHeader { header, .. } = response else {
unreachable!("unmatched response to a BlockHeader request")
@ -671,7 +669,7 @@ where
let valid_addresses = address_strings.valid_addresses()?;
let request = zebra_state::ReadRequest::AddressBalance(valid_addresses);
let response = state.oneshot(request).await.map_server_error()?;
let response = state.oneshot(request).await.map_misc_error()?;
match response {
zebra_state::ReadResponse::AddressBalance(balance) => Ok(AddressBalance {
@ -692,11 +690,12 @@ where
let queue_sender = self.queue_sender.clone();
async move {
let raw_transaction_bytes = Vec::from_hex(raw_transaction_hex).map_err(|_| {
Error::invalid_params("raw transaction is not specified as a hex string")
})?;
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/rawtransaction.cpp#L1259-L1260>
let raw_transaction_bytes = Vec::from_hex(raw_transaction_hex)
.map_error(server::error::LegacyCode::Deserialization)?;
let raw_transaction = Transaction::zcash_deserialize(&*raw_transaction_bytes)
.map_err(|_| Error::invalid_params("raw transaction is structurally invalid"))?;
.map_error(server::error::LegacyCode::Deserialization)?;
let transaction_hash = raw_transaction.hash();
@ -707,7 +706,7 @@ where
let transaction_parameter = mempool::Gossip::Tx(raw_transaction.into());
let request = mempool::Request::Queue(vec![transaction_parameter]);
let response = mempool.oneshot(request).await.map_server_error()?;
let response = mempool.oneshot(request).await.map_misc_error()?;
let mut queue_results = match response {
mempool::Response::Queued(results) => results,
@ -724,19 +723,30 @@ where
.pop()
.expect("there should be exactly one item in Vec")
.inspect_err(|err| tracing::debug!("sent transaction to mempool: {:?}", &err))
.map_server_error()?
.await;
.map_misc_error()?
.await
.map_misc_error()?;
tracing::debug!("sent transaction to mempool: {:?}", &queue_result);
queue_result
.map_server_error()?
.map(|_| SentTransactionHash(transaction_hash))
.map_server_error()
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/rawtransaction.cpp#L1290-L1301>
// Note that this error code might not exactly match the one returned by zcashd
// since zcashd's error code selection logic is more granular. We'd need to
// propagate the error coming from the verifier to be able to return more specific
// error codes.
.map_error(server::error::LegacyCode::Verify)
}
.boxed()
}
// # Performance
//
// `lightwalletd` calls this RPC with verosity 1 for its initial sync of 2 million blocks, the
// performance of this RPC with verbosity 1 significantly affects `lightwalletd`s sync time.
//
// TODO:
// - use `height_from_signed_int()` to handle negative heights
// (this might be better in the state request, because it needs the state height)
@ -745,11 +755,8 @@ where
hash_or_height: String,
verbosity: Option<u8>,
) -> BoxFuture<Result<GetBlock>> {
// From <https://zcash.github.io/rpc/getblock.html>
const DEFAULT_GETBLOCK_VERBOSITY: u8 = 1;
let mut state = self.state.clone();
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
let verbosity = verbosity.unwrap_or(1);
let network = self.network.clone();
let original_hash_or_height = hash_or_height.clone();
@ -761,29 +768,26 @@ where
};
async move {
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
let hash_or_height: HashOrHeight = hash_or_height
.parse()
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/blockchain.cpp#L629>
.map_error(server::error::LegacyCode::InvalidParameter)?;
if verbosity == 0 {
// # Performance
//
// This RPC is used in `lightwalletd`'s initial sync of 2 million blocks,
// so it needs to load block data very efficiently.
let request = zebra_state::ReadRequest::Block(hash_or_height);
let response = state
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
match response {
zebra_state::ReadResponse::Block(Some(block)) => {
Ok(GetBlock::Raw(block.into()))
}
zebra_state::ReadResponse::Block(None) => Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
message: "Block not found".to_string(),
data: None,
}),
zebra_state::ReadResponse::Block(None) => Err("Block not found")
.map_error(server::error::LegacyCode::InvalidParameter),
_ => unreachable!("unmatched response to a block request"),
}
} else if let Some(get_block_header_future) = get_block_header_future {
@ -835,9 +839,9 @@ where
}
let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
let tx = match tx_ids_response.map_server_error()? {
let tx = match tx_ids_response.map_misc_error()? {
zebra_state::ReadResponse::TransactionIdsForBlock(tx_ids) => tx_ids
.ok_or_server_error("Block not found")?
.ok_or_misc_error("block not found")?
.iter()
.map(|tx_id| tx_id.encode_hex())
.collect(),
@ -846,7 +850,7 @@ where
let orchard_tree_response = futs.next().await.expect("`futs` should not be empty");
let zebra_state::ReadResponse::OrchardTree(orchard_tree) =
orchard_tree_response.map_server_error()?
orchard_tree_response.map_misc_error()?
else {
unreachable!("unmatched response to a OrchardTree request");
};
@ -854,8 +858,7 @@ where
let nu5_activation = NetworkUpgrade::Nu5.activation_height(&network);
// This could be `None` if there's a chain reorg between state queries.
let orchard_tree =
orchard_tree.ok_or_server_error("missing orchard tree for block")?;
let orchard_tree = orchard_tree.ok_or_misc_error("missing Orchard tree")?;
let final_orchard_root = match nu5_activation {
Some(activation_height) if height >= activation_height => {
@ -895,11 +898,8 @@ where
next_block_hash,
})
} else {
Err(Error {
code: ErrorCode::InvalidParams,
message: "Invalid verbosity value".to_string(),
data: None,
})
Err("invalid verbosity value")
.map_error(server::error::LegacyCode::InvalidParameter)
}
}
.boxed()
@ -915,7 +915,9 @@ where
let network = self.network.clone();
async move {
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
let hash_or_height: HashOrHeight = hash_or_height
.parse()
.map_error(server::error::LegacyCode::InvalidAddressOrKey)?;
let zebra_state::ReadResponse::BlockHeader {
header,
hash,
@ -925,43 +927,42 @@ where
.clone()
.oneshot(zebra_state::ReadRequest::BlockHeader(hash_or_height))
.await
.map_err(|_| Error {
// Compatibility with zcashd. Note that since this function
// is reused by getblock(), we return the errors expected
// by it (they differ whether a hash or a height was passed)
code: if hash_or_height.hash().is_some() {
INVALID_ADDRESS_OR_KEY_ERROR_CODE
.map_err(|_| "block height not in best chain")
.map_error(
// ## Compatibility with `zcashd`.
//
// Since this function is reused by getblock(), we return the errors
// expected by it (they differ whether a hash or a height was passed).
if hash_or_height.hash().is_some() {
server::error::LegacyCode::InvalidAddressOrKey
} else {
MISSING_BLOCK_ERROR_CODE
server::error::LegacyCode::InvalidParameter
},
message: "block height not in best chain".to_string(),
data: None,
})?
)?
else {
panic!("unexpected response to BlockHeader request")
};
let response = if !verbose {
GetBlockHeader::Raw(HexData(header.zcash_serialize_to_vec().map_server_error()?))
GetBlockHeader::Raw(HexData(header.zcash_serialize_to_vec().map_misc_error()?))
} else {
let zebra_state::ReadResponse::SaplingTree(sapling_tree) = state
.clone()
.oneshot(zebra_state::ReadRequest::SaplingTree(hash_or_height))
.await
.map_server_error()?
.map_misc_error()?
else {
panic!("unexpected response to SaplingTree request")
};
// This could be `None` if there's a chain reorg between state queries.
let sapling_tree =
sapling_tree.ok_or_server_error("missing sapling tree for block")?;
let sapling_tree = sapling_tree.ok_or_misc_error("missing Sapling tree")?;
let zebra_state::ReadResponse::Depth(depth) = state
.clone()
.oneshot(zebra_state::ReadRequest::Depth(hash))
.await
.map_server_error()?
.map_misc_error()?
else {
panic!("unexpected response to SaplingTree request")
};
@ -1021,14 +1022,14 @@ where
self.latest_chain_tip
.best_tip_hash()
.map(GetBlockHash)
.ok_or_server_error("No blocks in state")
.ok_or_misc_error("No blocks in state")
}
fn get_best_block_height_and_hash(&self) -> Result<GetBlockHeightAndHash> {
self.latest_chain_tip
.best_tip_height_and_hash()
.map(|(height, hash)| GetBlockHeightAndHash { height, hash })
.ok_or_server_error("No blocks in state")
.ok_or_misc_error("No blocks in state")
}
fn get_raw_mempool(&self) -> BoxFuture<Result<Vec<String>>> {
@ -1057,7 +1058,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
match response {
#[cfg(feature = "getblocktemplate-rpcs")]
@ -1104,73 +1105,76 @@ where
.boxed()
}
// TODO: use HexData or SentTransactionHash to handle the transaction ID
fn get_raw_transaction(
&self,
txid_hex: String,
txid: String,
verbose: Option<u8>,
) -> BoxFuture<Result<GetRawTransaction>> {
let mut state = self.state.clone();
let mut mempool = self.mempool.clone();
let verbose = verbose.unwrap_or(0);
let verbose = verbose != 0;
let verbose = verbose.unwrap_or(0) != 0;
async move {
let txid = transaction::Hash::from_hex(txid_hex).map_err(|_| {
Error::invalid_params("transaction ID is not specified as a hex string")
})?;
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/rawtransaction.cpp#L544>
let txid = transaction::Hash::from_hex(txid)
.map_error(server::error::LegacyCode::InvalidAddressOrKey)?;
// Check the mempool first.
//
// # Correctness
//
// Transactions are removed from the mempool after they are mined into blocks,
// so the transaction could be just in the mempool, just in the state, or in both.
// (And the mempool and state transactions could have different authorising data.)
// But it doesn't matter which transaction we choose, because the effects are the same.
let mut txid_set = HashSet::new();
txid_set.insert(txid);
let request = mempool::Request::TransactionsByMinedId(txid_set);
let response = mempool
match mempool
.ready()
.and_then(|service| service.call(request))
.and_then(|service| {
service.call(mempool::Request::TransactionsByMinedId([txid].into()))
})
.await
.map_server_error()?;
.map_misc_error()?
{
mempool::Response::Transactions(txns) => {
if let Some(tx) = txns.first() {
let hex = tx.transaction.clone().into();
match response {
mempool::Response::Transactions(unmined_transactions) => {
if !unmined_transactions.is_empty() {
let tx = unmined_transactions[0].transaction.clone();
return Ok(GetRawTransaction::from_transaction(tx, None, 0, verbose));
return Ok(if verbose {
GetRawTransaction::Object {
hex,
height: None,
confirmations: None,
}
} else {
GetRawTransaction::Raw(hex)
});
}
}
_ => unreachable!("unmatched response to a transactionids request"),
_ => unreachable!("unmatched response to a `TransactionsByMinedId` request"),
};
// Now check the state
let request = zebra_state::ReadRequest::Transaction(txid);
let response = state
// If the tx wasn't in the mempool, check the state.
match state
.ready()
.and_then(|service| service.call(request))
.and_then(|service| service.call(zebra_state::ReadRequest::Transaction(txid)))
.await
.map_server_error()?;
.map_misc_error()?
{
zebra_state::ReadResponse::Transaction(Some(tx)) => {
let hex = tx.tx.into();
match response {
zebra_state::ReadResponse::Transaction(Some(MinedTx {
tx,
height,
confirmations,
})) => Ok(GetRawTransaction::from_transaction(
tx,
Some(height),
confirmations,
verbose,
)),
zebra_state::ReadResponse::Transaction(None) => {
Err("Transaction not found").map_server_error()
Ok(if verbose {
GetRawTransaction::Object {
hex,
height: Some(tx.height.0),
confirmations: Some(tx.confirmations),
}
} else {
GetRawTransaction::Raw(hex)
})
}
_ => unreachable!("unmatched response to a transaction request"),
zebra_state::ReadResponse::Transaction(None) => {
Err("No such mempool or main chain transaction")
.map_error(server::error::LegacyCode::InvalidAddressOrKey)
}
_ => unreachable!("unmatched response to a `Transaction` read request"),
}
}
.boxed()
@ -1184,8 +1188,11 @@ where
let network = self.network.clone();
async move {
// Convert the [`hash_or_height`] string into an actual hash or height.
let hash_or_height = hash_or_height.parse().map_server_error()?;
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/blockchain.cpp#L629>
let hash_or_height = hash_or_height
.parse()
.map_error(server::error::LegacyCode::InvalidParameter)?;
// Fetch the block referenced by [`hash_or_height`] from the state.
//
@ -1199,15 +1206,14 @@ where
.ready()
.and_then(|service| service.call(zebra_state::ReadRequest::Block(hash_or_height)))
.await
.map_server_error()?
.map_misc_error()?
{
zebra_state::ReadResponse::Block(Some(block)) => block,
zebra_state::ReadResponse::Block(None) => {
return Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
message: "the requested block was not found".to_string(),
data: None,
})
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/blockchain.cpp#L629>
return Err("the requested block is not in the main chain")
.map_error(server::error::LegacyCode::InvalidParameter);
}
_ => unreachable!("unmatched response to a block request"),
};
@ -1231,7 +1237,7 @@ where
service.call(zebra_state::ReadRequest::SaplingTree(hash.into()))
})
.await
.map_server_error()?
.map_misc_error()?
{
zebra_state::ReadResponse::SaplingTree(tree) => tree.map(|t| t.to_rpc_bytes()),
_ => unreachable!("unmatched response to a Sapling tree request"),
@ -1248,7 +1254,7 @@ where
service.call(zebra_state::ReadRequest::OrchardTree(hash.into()))
})
.await
.map_server_error()?
.map_misc_error()?
{
zebra_state::ReadResponse::OrchardTree(tree) => tree.map(|t| t.to_rpc_bytes()),
_ => unreachable!("unmatched response to an Orchard tree request"),
@ -1281,7 +1287,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let subtrees = match response {
zebra_state::ReadResponse::SaplingSubtrees(subtrees) => subtrees,
@ -1307,7 +1313,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let subtrees = match response {
zebra_state::ReadResponse::OrchardSubtrees(subtrees) => subtrees,
@ -1329,7 +1335,7 @@ where
})
} else {
Err(Error {
code: INVALID_PARAMETERS_ERROR_CODE,
code: server::error::LegacyCode::Misc.into(),
message: format!("invalid pool name, must be one of: {:?}", POOL_LIST),
data: None,
})
@ -1367,7 +1373,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let hashes = match response {
zebra_state::ReadResponse::AddressesTransactionIds(hashes) => {
@ -1414,7 +1420,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_misc_error()?;
let utxos = match response {
zebra_state::ReadResponse::AddressUtxos(utxos) => utxos,
_ => unreachable!("unmatched response to a UtxosByAddresses request"),
@ -1492,7 +1498,7 @@ where
{
latest_chain_tip
.best_tip_height()
.ok_or_server_error("No blocks in state")
.ok_or_misc_error("No blocks in state")
}
/// Response to a `getinfo` RPC request.
@ -1586,13 +1592,15 @@ impl AddressStrings {
/// - check if provided list have all valid transparent addresses.
/// - return valid addresses as a set of `Address`.
pub fn valid_addresses(self) -> Result<HashSet<Address>> {
// Reference for the legacy error code:
// <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/misc.cpp#L783-L784>
let valid_addresses: HashSet<Address> = self
.addresses
.into_iter()
.map(|address| {
address.parse().map_err(|error| {
Error::invalid_params(format!("invalid address {address:?}: {error}"))
})
address
.parse()
.map_error(server::error::LegacyCode::InvalidAddressOrKey)
})
.collect::<Result<_>>()?;
@ -1991,12 +1999,14 @@ pub enum GetRawTransaction {
/// The raw transaction, encoded as hex bytes.
#[serde(with = "hex")]
hex: SerializedTransaction,
/// The height of the block in the best chain that contains the transaction, or -1 if
/// the transaction is in the mempool.
height: i32,
/// The confirmations of the block in the best chain that contains the transaction,
/// or 0 if the transaction is in the mempool.
confirmations: u32,
/// The height of the block in the best chain that contains the tx or `None` if the tx is in
/// the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<u32>,
/// The height diff between the block containing the tx and the best chain tip + 1 or `None`
/// if the tx is in the mempool.
#[serde(skip_serializing_if = "Option::is_none")]
confirmations: Option<u32>,
},
}
@ -2006,8 +2016,8 @@ impl Default for GetRawTransaction {
hex: SerializedTransaction::from(
[0u8; zebra_chain::transaction::MIN_TRANSPARENT_TX_SIZE as usize].to_vec(),
),
height: i32::default(),
confirmations: u32::default(),
height: Option::default(),
confirmations: Option::default(),
}
}
}
@ -2070,33 +2080,6 @@ pub struct GetAddressTxIdsRequest {
end: u32,
}
impl GetRawTransaction {
/// Converts `tx` and `height` into a new `GetRawTransaction` in the `verbose` format.
#[allow(clippy::unwrap_in_result)]
fn from_transaction(
tx: Arc<Transaction>,
height: Option<block::Height>,
confirmations: u32,
verbose: bool,
) -> Self {
if verbose {
GetRawTransaction::Object {
hex: tx.into(),
height: match height {
Some(height) => height
.0
.try_into()
.expect("valid block heights are limited to i32::MAX"),
None => -1,
},
confirmations,
}
} else {
GetRawTransaction::Raw(tx.into())
}
}
}
/// Information about the sapling and orchard note commitment trees if any.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct GetBlockTrees {

View File

@ -1,37 +0,0 @@
//! Error conversions for Zebra's RPC methods.
use jsonrpc_core::ErrorCode;
pub(crate) trait MapServerError<T, E> {
fn map_server_error(self) -> std::result::Result<T, jsonrpc_core::Error>;
}
pub(crate) trait OkOrServerError<T> {
fn ok_or_server_error<S: ToString>(
self,
message: S,
) -> std::result::Result<T, jsonrpc_core::Error>;
}
impl<T, E> MapServerError<T, E> for Result<T, E>
where
E: ToString,
{
fn map_server_error(self) -> Result<T, jsonrpc_core::Error> {
self.map_err(|error| jsonrpc_core::Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})
}
}
impl<T> OkOrServerError<T> for Option<T> {
fn ok_or_server_error<S: ToString>(self, message: S) -> Result<T, jsonrpc_core::Error> {
self.ok_or(jsonrpc_core::Error {
code: ErrorCode::ServerError(0),
message: message.to_string(),
data: None,
})
}
}

View File

@ -32,35 +32,37 @@ use zebra_network::AddressBookPeers;
use zebra_node_services::mempool;
use zebra_state::{ReadRequest, ReadResponse};
use crate::methods::{
best_chain_tip_height,
errors::MapServerError,
get_block_template_rpcs::{
constants::{
DEFAULT_SOLUTION_RATE_WINDOW_SIZE, GET_BLOCK_TEMPLATE_MEMPOOL_LONG_POLL_INTERVAL,
ZCASHD_FUNDING_STREAM_ORDER,
},
get_block_template::{
check_miner_address, check_synced_to_tip, fetch_mempool_transactions,
fetch_state_tip_and_local_time, validate_block_proposal,
},
// TODO: move the types/* modules directly under get_block_template_rpcs,
// and combine any modules with the same names.
types::{
get_block_template::{
proposal::TimeSource, proposal_block_from_template, GetBlockTemplate,
use crate::{
methods::{
best_chain_tip_height,
get_block_template_rpcs::{
constants::{
DEFAULT_SOLUTION_RATE_WINDOW_SIZE, GET_BLOCK_TEMPLATE_MEMPOOL_LONG_POLL_INTERVAL,
ZCASHD_FUNDING_STREAM_ORDER,
},
get_block_template::{
check_miner_address, check_synced_to_tip, fetch_mempool_transactions,
fetch_state_tip_and_local_time, validate_block_proposal,
},
// TODO: move the types/* modules directly under get_block_template_rpcs,
// and combine any modules with the same names.
types::{
get_block_template::{
proposal::TimeSource, proposal_block_from_template, GetBlockTemplate,
},
get_mining_info,
long_poll::LongPollInput,
peer_info::PeerInfo,
submit_block,
subsidy::{BlockSubsidy, FundingStream},
unified_address, validate_address, z_validate_address,
},
get_mining_info,
long_poll::LongPollInput,
peer_info::PeerInfo,
submit_block,
subsidy::{BlockSubsidy, FundingStream},
unified_address, validate_address, z_validate_address,
},
height_from_signed_int,
hex_data::HexData,
GetBlockHash,
},
height_from_signed_int,
hex_data::HexData,
GetBlockHash, MISSING_BLOCK_ERROR_CODE,
server::{self, error::MapError},
};
pub mod constants;
@ -584,12 +586,12 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_error(server::error::LegacyCode::default())?;
match response {
zebra_state::ReadResponse::BlockHash(Some(hash)) => Ok(GetBlockHash(hash)),
zebra_state::ReadResponse::BlockHash(None) => Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
code: server::error::LegacyCode::InvalidParameter.into(),
message: "Block not found".to_string(),
data: None,
}),
@ -850,7 +852,7 @@ where
Is Zebra shutting down?"
);
return Err(recv_error).map_server_error();
return Err(recv_error).map_error(server::error::LegacyCode::default());
}
}
}
@ -1042,7 +1044,7 @@ where
.ready()
.and_then(|service| service.call(request))
.await
.map_server_error()?;
.map_error(server::error::LegacyCode::default())?;
current_block_size = match response {
zebra_state::ReadResponse::TipBlockSize(Some(block_size)) => Some(block_size),
_ => None,
@ -1231,13 +1233,14 @@ where
// Always zero for post-halving blocks
let founders = Amount::zero();
let total_block_subsidy = block_subsidy(height, &network).map_server_error()?;
let miner_subsidy =
miner_subsidy(height, &network, total_block_subsidy).map_server_error()?;
let total_block_subsidy =
block_subsidy(height, &network).map_error(server::error::LegacyCode::default())?;
let miner_subsidy = miner_subsidy(height, &network, total_block_subsidy)
.map_error(server::error::LegacyCode::default())?;
let (lockbox_streams, mut funding_streams): (Vec<_>, Vec<_>) =
funding_stream_values(height, &network, total_block_subsidy)
.map_server_error()?
.map_error(server::error::LegacyCode::default())?
.into_iter()
// Separate the funding streams into deferred and non-deferred streams
.partition(|(receiver, _)| matches!(receiver, FundingStreamReceiver::Deferred));
@ -1274,8 +1277,12 @@ where
founders: founders.into(),
funding_streams,
lockbox_streams,
funding_streams_total: funding_streams_total.map_server_error()?.into(),
lockbox_total: lockbox_total.map_server_error()?.into(),
funding_streams_total: funding_streams_total
.map_error(server::error::LegacyCode::default())?
.into(),
lockbox_total: lockbox_total
.map_error(server::error::LegacyCode::default())?
.into(),
total_block_subsidy: total_block_subsidy.into(),
})
}
@ -1436,7 +1443,10 @@ where
let mut block_hashes = Vec::new();
for _ in 0..num_blocks {
let block_template = rpc.get_block_template(None).await.map_server_error()?;
let block_template = rpc
.get_block_template(None)
.await
.map_error(server::error::LegacyCode::default())?;
let get_block_template::Response::TemplateMode(block_template) = block_template
else {
@ -1452,14 +1462,17 @@ where
TimeSource::CurTime,
NetworkUpgrade::current(&network, Height(block_template.height)),
)
.map_server_error()?;
let hex_proposal_block =
HexData(proposal_block.zcash_serialize_to_vec().map_server_error()?);
.map_error(server::error::LegacyCode::default())?;
let hex_proposal_block = HexData(
proposal_block
.zcash_serialize_to_vec()
.map_error(server::error::LegacyCode::default())?,
);
let _submit = rpc
.submit_block(hex_proposal_block, None)
.await
.map_server_error()?;
.map_error(server::error::LegacyCode::default())?;
block_hashes.push(GetBlockHash(proposal_block.hash()));
}

View File

@ -25,12 +25,12 @@ use zebra_consensus::{
use zebra_node_services::mempool::{self, TransactionDependencies};
use zebra_state::GetBlockTemplateChainInfo;
use crate::methods::{
errors::OkOrServerError,
get_block_template_rpcs::{
use crate::{
methods::get_block_template_rpcs::{
constants::{MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP, NOT_SYNCED_ERROR_CODE},
types::{default_roots::DefaultRoots, transaction::TransactionTemplate},
},
server::error::OkOrError,
};
pub use crate::methods::get_block_template_rpcs::types::get_block_template::*;
@ -87,13 +87,9 @@ pub fn check_parameters(parameters: &Option<JsonParameters>) -> Result<()> {
pub fn check_miner_address(
miner_address: Option<transparent::Address>,
) -> Result<transparent::Address> {
miner_address.ok_or_else(|| Error {
code: ErrorCode::ServerError(0),
message: "configure mining.miner_address in zebrad.toml \
with a transparent address"
.to_string(),
data: None,
})
miner_address.ok_or_misc_error(
"set `mining.miner_address` in `zebrad.toml` to a transparent address".to_string(),
)
}
/// Attempts to validate block proposal against all of the server's
@ -181,7 +177,7 @@ where
// but this is ok for an estimate
let (estimated_distance_to_chain_tip, local_tip_height) = latest_chain_tip
.estimate_distance_to_network_chain_tip(network)
.ok_or_server_error("no chain tip available yet")?;
.ok_or_misc_error("no chain tip available yet")?;
if !sync_status.is_close_to_tip()
|| estimated_distance_to_chain_tip > MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
//! cargo insta test --review --release -p zebra-rpc --lib -- test_rpc_response_data
//! ```
use std::collections::BTreeMap;
use std::{collections::BTreeMap, sync::Arc};
use insta::dynamic_redaction;
use tower::buffer::Buffer;
@ -229,12 +229,10 @@ async fn test_rpc_response_data_for_network(network: &Network) {
snapshot_rpc_getblockchaininfo("", get_blockchain_info, &settings);
// get the first transaction of the first block which is not the genesis
let first_block_first_transaction = &blocks[1].transactions[0];
let first_block_first_tx = &blocks[1].transactions[0];
// build addresses
let address = &first_block_first_transaction.outputs()[1]
.address(network)
.unwrap();
let address = &first_block_first_tx.outputs()[1].address(network).unwrap();
let addresses = vec![address.to_string()];
// `getaddressbalance`
@ -407,8 +405,9 @@ async fn test_rpc_response_data_for_network(network: &Network) {
// `getrawtransaction` verbosity=0
//
// - similar to `getrawmempool` described above, a mempool request will be made to get the requested
// transaction from the mempool, response will be empty as we have this transaction in state
// - Similarly to `getrawmempool` described above, a mempool request will be made to get the
// requested transaction from the mempool. Response will be empty as we have this transaction
// in the state.
let mempool_req = mempool
.expect_request_that(|request| {
matches!(request, mempool::Request::TransactionsByMinedId(_))
@ -417,13 +416,12 @@ async fn test_rpc_response_data_for_network(network: &Network) {
responder.respond(mempool::Response::Transactions(vec![]));
});
// make the api call
let get_raw_transaction =
rpc.get_raw_transaction(first_block_first_transaction.hash().encode_hex(), Some(0u8));
let (response, _) = futures::join!(get_raw_transaction, mempool_req);
let get_raw_transaction = response.expect("We should have a GetRawTransaction struct");
let txid = first_block_first_tx.hash().encode_hex::<String>();
snapshot_rpc_getrawtransaction("verbosity_0", get_raw_transaction, &settings);
let rpc_req = rpc.get_raw_transaction(txid.clone(), Some(0u8));
let (rsp, _) = futures::join!(rpc_req, mempool_req);
settings.bind(|| insta::assert_json_snapshot!(format!("getrawtransaction_verbosity=0"), rsp));
mempool.expect_no_requests().await;
// `getrawtransaction` verbosity=1
let mempool_req = mempool
@ -434,13 +432,31 @@ async fn test_rpc_response_data_for_network(network: &Network) {
responder.respond(mempool::Response::Transactions(vec![]));
});
// make the api call
let get_raw_transaction =
rpc.get_raw_transaction(first_block_first_transaction.hash().encode_hex(), Some(1u8));
let (response, _) = futures::join!(get_raw_transaction, mempool_req);
let get_raw_transaction = response.expect("We should have a GetRawTransaction struct");
let rpc_req = rpc.get_raw_transaction(txid, Some(1u8));
let (rsp, _) = futures::join!(rpc_req, mempool_req);
settings.bind(|| insta::assert_json_snapshot!(format!("getrawtransaction_verbosity=1"), rsp));
mempool.expect_no_requests().await;
snapshot_rpc_getrawtransaction("verbosity_1", get_raw_transaction, &settings);
// `getrawtransaction` with unknown txid
let mempool_req = mempool
.expect_request_that(|request| {
matches!(request, mempool::Request::TransactionsByMinedId(_))
})
.map(|responder| {
responder.respond(mempool::Response::Transactions(vec![]));
});
let rpc_req = rpc.get_raw_transaction(transaction::Hash::from([0; 32]).encode_hex(), Some(1));
let (rsp, _) = futures::join!(rpc_req, mempool_req);
settings.bind(|| insta::assert_json_snapshot!(format!("getrawtransaction_unknown_txid"), rsp));
mempool.expect_no_requests().await;
// `getrawtransaction` with an invalid TXID
let rsp = rpc
.get_raw_transaction("aBadC0de".to_owned(), Some(1))
.await;
settings.bind(|| insta::assert_json_snapshot!(format!("getrawtransaction_invalid_txid"), rsp));
mempool.expect_no_requests().await;
// `getaddresstxids`
let get_address_tx_ids = rpc
@ -666,17 +682,6 @@ fn snapshot_rpc_getrawmempool(raw_mempool: Vec<String>, settings: &insta::Settin
settings.bind(|| insta::assert_json_snapshot!("get_raw_mempool", raw_mempool));
}
/// Snapshot `getrawtransaction` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getrawtransaction(
variant: &'static str,
raw_transaction: GetRawTransaction,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_raw_transaction_{variant}"), raw_transaction)
});
}
/// Snapshot valid `getaddressbalance` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getaddresstxids_valid(
variant: &'static str,

View File

@ -1,5 +0,0 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: raw_transaction
---
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000"

View File

@ -1,5 +0,0 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: raw_transaction
---
"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000"

View File

@ -1,9 +0,0 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: raw_transaction
---
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000",
"height": 1,
"confirmations": 10
}

View File

@ -1,9 +0,0 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: raw_transaction
---
{
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000",
"height": 1,
"confirmations": 10
}

View File

@ -0,0 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Err": {
"code": -5,
"message": "Invalid string length"
}
}

View File

@ -0,0 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Err": {
"code": -5,
"message": "Invalid string length"
}
}

View File

@ -0,0 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Err": {
"code": -5,
"message": "No such mempool or main chain transaction"
}
}

View File

@ -0,0 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Err": {
"code": -5,
"message": "No such mempool or main chain transaction"
}
}

View File

@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Ok": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000"
}

View File

@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Ok": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000"
}

View File

@ -0,0 +1,12 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Ok": {
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000",
"height": 1,
"confirmations": 10
}
}

View File

@ -0,0 +1,12 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: rsp
snapshot_kind: text
---
{
"Ok": {
"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000",
"height": 1,
"confirmations": 10
}
}

View File

@ -1,10 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: treestate
snapshot_kind: text
---
{
"Err": {
"code": -8,
"message": "the requested block was not found"
"message": "the requested block is not in the main chain"
}
}

View File

@ -1,10 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: treestate
snapshot_kind: text
---
{
"Err": {
"code": -8,
"message": "the requested block was not found"
"message": "the requested block is not in the main chain"
}
}

View File

@ -1,10 +1,11 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
expression: treestate
snapshot_kind: text
---
{
"Err": {
"code": 0,
"code": -8,
"message": "parse error: could not convert the input string to a hash or height"
}
}

View File

@ -1,15 +1,17 @@
//! Fixed test vectors for RPC methods.
use std::ops::RangeInclusive;
use std::sync::Arc;
use tower::buffer::Buffer;
use zebra_chain::serialization::ZcashSerialize;
use zebra_chain::{
amount::Amount,
block::Block,
chain_tip::{mock::MockChainTip, NoChainTip},
parameters::Network::*,
serialization::{ZcashDeserializeInto, ZcashSerialize},
serialization::ZcashDeserializeInto,
transaction::UnminedTxId,
};
use zebra_node_services::BoxError;
@ -722,9 +724,12 @@ async fn rpc_getrawtransaction() {
conventional_fee: Amount::zero(),
}]));
});
let get_tx_req = rpc.get_raw_transaction(tx.hash().encode_hex(), Some(0u8));
let (response, _) = futures::join!(get_tx_req, mempool_req);
let get_tx = response.expect("We should have a GetRawTransaction struct");
let rpc_req = rpc.get_raw_transaction(tx.hash().encode_hex(), Some(0u8));
let (rsp, _) = futures::join!(rpc_req, mempool_req);
let get_tx = rsp.expect("we should have a `GetRawTransaction` struct");
if let GetRawTransaction::Raw(raw_tx) = get_tx {
assert_eq!(raw_tx.as_ref(), tx.zcash_serialize_to_vec().unwrap());
} else {
@ -752,12 +757,14 @@ async fn rpc_getrawtransaction() {
let run_state_test_case = |block_idx: usize, block: Arc<Block>, tx: Arc<Transaction>| {
let read_state = read_state.clone();
let tx_hash = tx.hash();
let get_tx_verbose_0_req = rpc.get_raw_transaction(tx_hash.encode_hex(), Some(0u8));
let get_tx_verbose_1_req = rpc.get_raw_transaction(tx_hash.encode_hex(), Some(1u8));
let txid = tx.hash();
let hex_txid = txid.encode_hex::<String>();
let get_tx_verbose_0_req = rpc.get_raw_transaction(hex_txid.clone(), Some(0u8));
let get_tx_verbose_1_req = rpc.get_raw_transaction(hex_txid, Some(1u8));
async move {
let (response, _) = futures::join!(get_tx_verbose_0_req, make_mempool_req(tx_hash));
let (response, _) = futures::join!(get_tx_verbose_0_req, make_mempool_req(txid));
let get_tx = response.expect("We should have a GetRawTransaction struct");
if let GetRawTransaction::Raw(raw_tx) = get_tx {
assert_eq!(raw_tx.as_ref(), tx.zcash_serialize_to_vec().unwrap());
@ -765,7 +772,8 @@ async fn rpc_getrawtransaction() {
unreachable!("Should return a Raw enum")
}
let (response, _) = futures::join!(get_tx_verbose_1_req, make_mempool_req(tx_hash));
let (response, _) = futures::join!(get_tx_verbose_1_req, make_mempool_req(txid));
let GetRawTransaction::Object {
hex,
height,
@ -775,8 +783,11 @@ async fn rpc_getrawtransaction() {
unreachable!("Should return a Raw enum")
};
let height = height.expect("state requests should have height");
let confirmations = confirmations.expect("state requests should have confirmations");
assert_eq!(hex.as_ref(), tx.zcash_serialize_to_vec().unwrap());
assert_eq!(height, block_idx as i32);
assert_eq!(height, block_idx as u32);
let depth_response = read_state
.oneshot(zebra_state::ReadRequest::Depth(block.hash()))
@ -870,25 +881,18 @@ async fn rpc_getaddresstxids_invalid_arguments() {
);
// call the method with an invalid address string
let address = "11111111".to_string();
let addresses = vec![address.clone()];
let start: u32 = 1;
let end: u32 = 2;
let error = rpc
let rpc_rsp = rpc
.get_address_tx_ids(GetAddressTxIdsRequest {
addresses: addresses.clone(),
start,
end,
addresses: vec!["t1invalidaddress".to_owned()],
start: 1,
end: 2,
})
.await
.unwrap_err();
assert_eq!(
error.message,
format!(
"invalid address \"{}\": parse error: t-addr decoding error",
address.clone()
)
);
assert_eq!(rpc_rsp.code, ErrorCode::ServerError(-5));
mempool.expect_no_requests().await;
// create a valid address
let address = "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd".to_string();
@ -1078,17 +1082,13 @@ async fn rpc_getaddressutxos_invalid_arguments() {
);
// call the method with an invalid address string
let address = "11111111".to_string();
let addresses = vec![address.clone()];
let error = rpc
.0
.get_address_utxos(AddressStrings::new(addresses))
.get_address_utxos(AddressStrings::new(vec!["t1invalidaddress".to_owned()]))
.await
.unwrap_err();
assert_eq!(
error.message,
format!("invalid address \"{address}\": parse error: t-addr decoding error")
);
assert_eq!(error.code, ErrorCode::ServerError(-5));
mempool.expect_no_requests().await;
state.expect_no_requests().await;

View File

@ -36,6 +36,7 @@ use crate::{
use crate::methods::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl};
pub mod cookie;
pub mod error;
pub mod http_request_compatibility;
pub mod rpc_call_compatibility;

View File

@ -0,0 +1,118 @@
//! RPC error codes & their handling.
/// Bitcoin RPC error codes
///
/// Drawn from <https://github.com/zcash/zcash/blob/99ad6fdc3a549ab510422820eea5e5ce9f60a5fd/src/rpc/protocol.h#L32-L80>.
///
/// ## Notes
///
/// - All explicit discriminants fit within `i64`.
#[derive(Default)]
pub enum LegacyCode {
// General application defined errors
/// `std::exception` thrown in command handling
#[default]
Misc = -1,
/// Server is in safe mode, and command is not allowed in safe mode
ForbiddenBySafeMode = -2,
/// Unexpected type was passed as parameter
Type = -3,
/// Invalid address or key
InvalidAddressOrKey = -5,
/// Ran out of memory during operation
OutOfMemory = -7,
/// Invalid, missing or duplicate parameter
InvalidParameter = -8,
/// Database error
Database = -20,
/// Error parsing or validating structure in raw format
Deserialization = -22,
/// General error during transaction or block submission
Verify = -25,
/// Transaction or block was rejected by network rules
VerifyRejected = -26,
/// Transaction already in chain
VerifyAlreadyInChain = -27,
/// Client still warming up
InWarmup = -28,
// P2P client errors
/// Bitcoin is not connected
ClientNotConnected = -9,
/// Still downloading initial blocks
ClientInInitialDownload = -10,
/// Node is already added
ClientNodeAlreadyAdded = -23,
/// Node has not been added before
ClientNodeNotAdded = -24,
/// Node to disconnect not found in connected nodes
ClientNodeNotConnected = -29,
/// Invalid IP/Subnet
ClientInvalidIpOrSubnet = -30,
}
impl From<LegacyCode> for jsonrpc_core::ErrorCode {
fn from(code: LegacyCode) -> Self {
Self::ServerError(code as i64)
}
}
/// A trait for mapping errors to [`jsonrpc_core::Error`].
pub(crate) trait MapError<T>: Sized {
/// Maps errors to [`jsonrpc_core::Error`] with a specific error code.
fn map_error(
self,
code: impl Into<jsonrpc_core::ErrorCode>,
) -> std::result::Result<T, jsonrpc_core::Error>;
/// Maps errors to [`jsonrpc_core::Error`] with a [`LegacyCode::Misc`] error code.
fn map_misc_error(self) -> std::result::Result<T, jsonrpc_core::Error> {
self.map_error(LegacyCode::Misc)
}
}
/// A trait for conditionally converting a value into a `Result<T, jsonrpc_core::Error>`.
pub(crate) trait OkOrError<T>: Sized {
/// Converts the implementing type to `Result<T, jsonrpc_core::Error>`, using an error code and
/// message if conversion is to `Err`.
fn ok_or_error(
self,
code: impl Into<jsonrpc_core::ErrorCode>,
message: impl ToString,
) -> std::result::Result<T, jsonrpc_core::Error>;
/// Converts the implementing type to `Result<T, jsonrpc_core::Error>`, using a [`LegacyCode::Misc`] error code.
fn ok_or_misc_error(
self,
message: impl ToString,
) -> std::result::Result<T, jsonrpc_core::Error> {
self.ok_or_error(LegacyCode::Misc, message)
}
}
impl<T, E> MapError<T> for Result<T, E>
where
E: ToString,
{
fn map_error(self, code: impl Into<jsonrpc_core::ErrorCode>) -> Result<T, jsonrpc_core::Error> {
self.map_err(|error| jsonrpc_core::Error {
code: code.into(),
message: error.to_string(),
data: None,
})
}
}
impl<T> OkOrError<T> for Option<T> {
fn ok_or_error(
self,
code: impl Into<jsonrpc_core::ErrorCode>,
message: impl ToString,
) -> Result<T, jsonrpc_core::Error> {
self.ok_or(jsonrpc_core::Error {
code: code.into(),
message: message.to_string(),
data: None,
})
}
}

View File

@ -6,13 +6,14 @@
use std::future::Future;
use futures::future::{Either, FutureExt};
use jsonrpc_core::{
middleware::Middleware,
types::{Call, Failure, Output, Response},
BoxFuture, ErrorCode, Metadata, MethodCall, Notification,
BoxFuture, Metadata, MethodCall, Notification,
};
use crate::constants::{INVALID_PARAMETERS_ERROR_CODE, MAX_PARAMS_LOG_LENGTH};
use crate::server;
/// JSON-RPC [`Middleware`] with compatibility workarounds.
///
@ -57,13 +58,20 @@ impl<M: Metadata> Middleware<M> for FixRpcResponseMiddleware {
}
impl FixRpcResponseMiddleware {
/// Replace [`jsonrpc_core`] server error codes in `output` with the `zcashd` equivalents.
/// Replaces [`jsonrpc_core::ErrorCode`]s in the [`Output`] with their `zcashd` equivalents.
///
/// ## Replaced Codes
///
/// 1. [`jsonrpc_core::ErrorCode::InvalidParams`] -> [`server::error::LegacyCode::Misc`]
/// Rationale:
/// The `node-stratum-pool` mining pool library expects error code `-1` to detect available RPC methods:
/// <https://github.com/s-nomp/node-stratum-pool/blob/d86ae73f8ff968d9355bb61aac05e0ebef36ccb5/lib/pool.js#L459>
fn fix_error_codes(output: &mut Option<Output>) {
if let Some(Output::Failure(Failure { ref mut error, .. })) = output {
if matches!(error.code, ErrorCode::InvalidParams) {
if matches!(error.code, jsonrpc_core::ErrorCode::InvalidParams) {
let original_code = error.code.clone();
error.code = INVALID_PARAMETERS_ERROR_CODE;
error.code = server::error::LegacyCode::Misc.into();
tracing::debug!("Replacing RPC error: {original_code:?} with {error}");
}
}
@ -73,6 +81,8 @@ impl FixRpcResponseMiddleware {
///
/// Prints out only the method name and the received parameters.
fn call_description(call: &Call) -> String {
const MAX_PARAMS_LOG_LENGTH: usize = 100;
match call {
Call::MethodCall(MethodCall { method, params, .. }) => {
let mut params = format!("{params:?}");

View File

@ -21,8 +21,8 @@ use zebra_state::{
use zebra_chain::diagnostic::task::WaitForPanics;
use crate::{
constants::MISSING_BLOCK_ERROR_CODE,
methods::{hex_data::HexData, GetBlockHeightAndHash},
server,
};
/// How long to wait between calls to `getbestblockheightandhash` when it:
@ -383,7 +383,9 @@ impl SyncerRpcMethods for RpcRequestClient {
Err(err)
if err
.downcast_ref::<jsonrpc_core::Error>()
.is_some_and(|err| err.code == MISSING_BLOCK_ERROR_CODE) =>
.is_some_and(|err| {
err.code == server::error::LegacyCode::InvalidParameter.into()
}) =>
{
Ok(None)
}

View File

@ -4,21 +4,28 @@ use crate::methods::{GetBlock, GetRawTransaction};
#[test]
pub fn test_transaction_serialization() {
let expected_tx = GetRawTransaction::Raw(vec![0x42].into());
let expected_json = r#""42""#;
let j = serde_json::to_string(&expected_tx).unwrap();
let tx = GetRawTransaction::Raw(vec![0x42].into());
assert_eq!(j, expected_json);
assert_eq!(serde_json::to_string(&tx).unwrap(), r#""42""#);
let expected_tx = GetRawTransaction::Object {
let tx = GetRawTransaction::Object {
hex: vec![0x42].into(),
height: 1,
confirmations: 0,
height: Some(1),
confirmations: Some(0),
};
let expected_json = r#"{"hex":"42","height":1,"confirmations":0}"#;
let j = serde_json::to_string(&expected_tx).unwrap();
assert_eq!(j, expected_json);
assert_eq!(
serde_json::to_string(&tx).unwrap(),
r#"{"hex":"42","height":1,"confirmations":0}"#
);
let tx = GetRawTransaction::Object {
hex: vec![0x42].into(),
height: None,
confirmations: None,
};
assert_eq!(serde_json::to_string(&tx).unwrap(), r#"{"hex":"42"}"#);
}
#[test]

View File

@ -17,7 +17,6 @@ use zebra_chain::{
};
use zebra_node_services::rpc_client::RpcRequestClient;
use zebra_rpc::{
constants::MISSING_BLOCK_ERROR_CODE,
methods::{
get_block_template_rpcs::{
get_block_template::{
@ -27,7 +26,7 @@ use zebra_rpc::{
},
hex_data::HexData,
},
server::OPENED_RPC_ENDPOINT_MSG,
server::{self, OPENED_RPC_ENDPOINT_MSG},
};
use zebra_test::args;
@ -163,7 +162,9 @@ impl MiningRpcMethods for RpcRequestClient {
Err(err)
if err
.downcast_ref::<jsonrpc_core::Error>()
.is_some_and(|err| err.code == MISSING_BLOCK_ERROR_CODE) =>
.is_some_and(|err| {
err.code == server::error::LegacyCode::InvalidParameter.into()
}) =>
{
Ok(None)
}