Skip to content

Commit

Permalink
rpc: align getblock with zcashd behaviour
Browse files Browse the repository at this point in the history
  • Loading branch information
conradoplg committed Nov 8, 2024
1 parent f415a5a commit 618beb3
Show file tree
Hide file tree
Showing 16 changed files with 606 additions and 40 deletions.
4 changes: 4 additions & 0 deletions zebra-rpc/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use jsonrpc_core::{Error, ErrorCode};
/// <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:
Expand Down
248 changes: 236 additions & 12 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use zebra_node_services::mempool;
use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation};

use crate::{
constants::{INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE},
constants::{
INVALID_ADDRESS_OR_KEY_ERROR_CODE, INVALID_PARAMETERS_ERROR_CODE, MISSING_BLOCK_ERROR_CODE,
},
methods::trees::{GetSubtrees, GetTreestate, SubtreeRpcData},
queue::Queue,
};
Expand Down Expand Up @@ -145,7 +147,8 @@ pub trait Rpc {

/// Returns the requested block by hash or height, as a [`GetBlock`] JSON string.
/// If the block is not in Zebra's state, returns
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
/// [error code `-8`.](https://github.com/zcash/zcash/issues/5758) if a height was
/// passed or -5 if a hash was passed.
///
/// zcashd reference: [`getblock`](https://zcash.github.io/rpc/getblock.html)
/// method: post
Expand All @@ -154,16 +157,19 @@ pub trait Rpc {
/// # Parameters
///
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data.
/// - `verbosity`: (number, optional, default=1, example=1) 0 for hex encoded data, 1 for a json object, and 2 for json object with transaction data, and 3 for a partially filled json object (which is faster and useful for lightwalletd-only usage)
///
/// # Notes
///
/// With verbosity=1, [`lightwalletd` only reads the `tx` field of the
/// result](https://github.com/zcash/lightwalletd/blob/dfac02093d85fb31fb9a8475b884dd6abca966c7/common/common.go#L152),
/// and other clients only read the `hash` and `confirmations` fields,
/// so we only return a few fields for now.
/// Zebra previously partially supported verbosity=1 by returning only the
/// fields required by lightwalletd ([`lightwalletd` only reads the `tx`
/// field of the
/// result](https://github.com/zcash/lightwalletd/blob/dfac02093d85fb31fb9a8475b884dd6abca966c7/common/common.go#L152)).
/// That verbosity level was migrated to "3"; so while lightwalletd will
/// still work by using verbosity=1, it will sync faster if it is changed to
/// use verbosity=3.
///
/// `lightwalletd` and mining clients also do not use verbosity=2, so we don't support it.
/// The undocumented `chainwork` field is not returned.
#[rpc(name = "getblock")]
fn get_block(
&self,
Expand All @@ -172,6 +178,9 @@ pub trait Rpc {
) -> BoxFuture<Result<GetBlock>>;

/// Returns the requested block header by hash or height, as a [`GetBlockHeader`] JSON string.
/// If the block is not in Zebra's state,
/// returns [error code `-8`.](https://github.com/zcash/zcash/issues/5758)
/// if a height was passed or -5 if a hash was passed.
///
/// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html)
/// method: post
Expand All @@ -181,6 +190,10 @@ pub trait Rpc {
///
/// - `hash_or_height`: (string, required, example="1") The hash or height for the block to be returned.
/// - `verbose`: (bool, optional, default=false, example=true) false for hex encoded data, true for a json object
///
/// # Notes
///
/// The undocumented `chainwork` field is not returned.
#[rpc(name = "getblockheader")]
fn get_block_header(
&self,
Expand Down Expand Up @@ -738,7 +751,9 @@ where

let mut state = self.state.clone();
let verbosity = verbosity.unwrap_or(DEFAULT_GETBLOCK_VERBOSITY);
let self_clone = self.clone();

let original_hash_or_height = hash_or_height.clone();
async move {
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;

Expand Down Expand Up @@ -766,6 +781,99 @@ where
_ => unreachable!("unmatched response to a block request"),
}
} else if verbosity == 1 || verbosity == 2 {
let r: Result<GetBlockHeader> = self_clone
.get_block_header(original_hash_or_height, Some(true))
.await;

let GetBlockHeader::Object(h) = r? else {
panic!("must return Object")
};
let hash = h.hash.0;

// # Concurrency
//
// We look up by block hash so the hash, transaction IDs, and confirmations
// are consistent.
let requests = vec![
// Get transaction IDs from the transaction index by block hash
//
// # Concurrency
//
// A block's transaction IDs are never modified, so all possible responses are
// valid. Clients that query block heights must be able to handle chain forks,
// including getting transaction IDs from any chain fork.
zebra_state::ReadRequest::TransactionIdsForBlock(hash.into()),
// Sapling trees
zebra_state::ReadRequest::SaplingTree(hash.into()),
// Orchard trees
zebra_state::ReadRequest::OrchardTree(hash.into()),
];

let mut futs = FuturesOrdered::new();

for request in requests {
futs.push_back(state.clone().oneshot(request));
}

let tx_ids_response = futs.next().await.expect("`futs` should not be empty");
let tx = match tx_ids_response.map_server_error()? {
zebra_state::ReadResponse::TransactionIdsForBlock(tx_ids) => tx_ids
.ok_or_server_error("Block not found")?
.iter()
.map(|tx_id| tx_id.encode_hex())
.collect(),
_ => unreachable!("unmatched response to a transaction_ids_for_block request"),
};

let sapling_tree_response = futs.next().await.expect("`futs` should not be empty");
let sapling_note_commitment_tree_count =
match sapling_tree_response.map_server_error()? {
zebra_state::ReadResponse::SaplingTree(Some(nct)) => nct.count(),
zebra_state::ReadResponse::SaplingTree(None) => 0,
_ => unreachable!("unmatched response to a SaplingTree request"),
};

let orchard_tree_response = futs.next().await.expect("`futs` should not be empty");
let orchard_note_commitment_tree_count =
match orchard_tree_response.map_server_error()? {
zebra_state::ReadResponse::OrchardTree(Some(nct)) => nct.count(),
zebra_state::ReadResponse::OrchardTree(None) => 0,
_ => unreachable!("unmatched response to a OrchardTree request"),
};

let sapling = SaplingTrees {
size: sapling_note_commitment_tree_count,
};

let orchard = OrchardTrees {
size: orchard_note_commitment_tree_count,
};

let trees = GetBlockTrees { sapling, orchard };

Ok(GetBlock::Object {
hash: h.hash,
confirmations: h.confirmations,
height: Some(h.height),
version: Some(h.version),
merkle_root: Some(h.merkle_root),
time: Some(h.time),
nonce: Some(h.nonce),
solution: Some(h.solution),
bits: Some(h.bits),
difficulty: Some(h.difficulty),
// TODO
tx,
trees,
// TODO
size: None,
final_sapling_root: Some(h.final_sapling_root),
// TODO
final_orchard_root: None,
previous_block_hash: Some(h.previous_block_hash),
next_block_hash: h.next_block_hash,
})
} else if verbosity == 3 {
// # Performance
//
// This RPC is used in `lightwalletd`'s initial sync of 2 million blocks,
Expand Down Expand Up @@ -920,6 +1028,17 @@ where
time,
tx,
trees,
size: None,
version: None,
merkle_root: None,
final_sapling_root: None,
final_orchard_root: None,
nonce: None,
bits: None,
difficulty: None,
previous_block_hash: None,
next_block_hash: None,
solution: None,
})
} else {
Err(Error {
Expand Down Expand Up @@ -952,7 +1071,18 @@ where
.clone()
.oneshot(zebra_state::ReadRequest::BlockHeader(hash_or_height))
.await
.map_server_error()?
.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
} else {
MISSING_BLOCK_ERROR_CODE
},
message: "block height not in best chain".to_string(),
data: None,
})?
else {
panic!("unexpected response to BlockHeader request")
};
Expand Down Expand Up @@ -1688,8 +1818,9 @@ impl Default for SentTransactionHash {
/// Response to a `getblock` RPC request.
///
/// See the notes for the [`Rpc::get_block`] method.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)] //TODO: create a struct for the Object and Box it
pub enum GetBlock {
/// The request block, hex-encoded.
Raw(#[serde(with = "hex")] SerializedBlock),
Expand All @@ -1702,21 +1833,84 @@ pub enum GetBlock {
/// or -1 if it is not in the best chain.
confirmations: i64,

/// The block size. TODO: fill it
#[serde(skip_serializing_if = "Option::is_none")]
size: Option<i64>,

/// The height of the requested block.
#[serde(skip_serializing_if = "Option::is_none")]
height: Option<Height>,

/// The height of the requested block.
/// The version field of the requested block.
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<i64>,
version: Option<u32>,

/// The merkle root of the requesteed block.
#[serde(with = "opthex", rename = "merkleroot")]
#[serde(skip_serializing_if = "Option::is_none")]
merkle_root: Option<block::merkle::Root>,

// `blockcommitments` would be here. Undocumented. TODO: decide if we want to support it
// `authdataroot` would be here. Undocumented. TODO: decide if we want to support it
//
/// The root of the Sapling commitment tree after applying this block.
#[serde(with = "opthex", rename = "finalsaplingroot")]
#[serde(skip_serializing_if = "Option::is_none")]
final_sapling_root: Option<[u8; 32]>,

/// The root of the Orchard commitment tree after applying this block.
#[serde(with = "opthex", rename = "finalorchardroot")]
#[serde(skip_serializing_if = "Option::is_none")]
final_orchard_root: Option<[u8; 32]>,

// `chainhistoryroot` would be here. Undocumented. TODO: decide if we want to support it
//
/// List of transaction IDs in block order, hex-encoded.
//
// TODO: use a typed Vec<transaction::Hash> here
// TODO: support Objects
tx: Vec<String>,

/// The height of the requested block.
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<i64>,

/// The nonce of the requested block header.
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<[u8; 32]>,

/// The Equihash solution in the requested block header.
/// Note: presence of this field in getblock is not documented in zcashd.
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
solution: Option<Solution>,

/// The difficulty threshold of the requested block header displayed in compact form.
#[serde(with = "opthex")]
#[serde(skip_serializing_if = "Option::is_none")]
bits: Option<CompactDifficulty>,

/// Floating point number that represents the difficulty limit for this block as a multiple
/// of the minimum difficulty for the network.
#[serde(skip_serializing_if = "Option::is_none")]
difficulty: Option<f64>,

// `chainwork` would be here, but we don't plan on supporting it
// `anchor` would be here. Undocumented. TODO: decide if we want to support it
// `chainSupply` would be here, TODO: implement
// `valuePools` would be here, TODO: implement
//
/// Information about the note commitment trees.
trees: GetBlockTrees,

/// The previous block hash of the requested block header.
#[serde(rename = "previousblockhash", skip_serializing_if = "Option::is_none")]
previous_block_hash: Option<GetBlockHash>,

/// The next block hash after the requested block header.
#[serde(rename = "nextblockhash", skip_serializing_if = "Option::is_none")]
next_block_hash: Option<GetBlockHash>,
},
}

Expand All @@ -1729,6 +1923,17 @@ impl Default for GetBlock {
time: None,
tx: Vec::new(),
trees: GetBlockTrees::default(),
size: None,
version: None,
merkle_root: None,
final_sapling_root: None,
final_orchard_root: None,
nonce: None,
bits: None,
difficulty: None,
previous_block_hash: None,
next_block_hash: None,
solution: None,
}
}
}
Expand Down Expand Up @@ -2088,3 +2293,22 @@ pub fn height_from_signed_int(index: i32, tip_height: Height) -> Result<Height>
Ok(Height(sanitized_height))
}
}

mod opthex {
use hex::ToHex;
use serde::Serializer;

pub fn serialize<S, T>(data: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: ToHex,
{
match data {
Some(data) => {
let s = data.encode_hex::<String>();
serializer.serialize_str(&s)
}
None => serializer.serialize_none(),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,19 @@ expression: block
{
"hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283",
"confirmations": 10,
"height": 1,
"version": 4,
"merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609",
"finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000",
"tx": [
"851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609"
],
"trees": {}
"time": 1477671596,
"nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475",
"solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b",
"bits": "1f07ffff",
"difficulty": 1.0,
"trees": {},
"previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08",
"nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed"
}
Loading

0 comments on commit 618beb3

Please sign in to comment.