Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(rpc): Adds getblockheader RPC method #8967

Merged
merged 14 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions zebra-chain/src/sapling/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ impl TryFrom<[u8; 32]> for Root {
}
}

impl ToHex for &Root {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
<[u8; 32]>::from(*self).encode_hex()
}

fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
<[u8; 32]>::from(*self).encode_hex_upper()
}
}

impl ToHex for Root {
fn encode_hex<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex()
}

fn encode_hex_upper<T: FromIterator<char>>(&self) -> T {
(&self).encode_hex_upper()
}
}

impl ZcashSerialize for Root {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
writer.write_all(&<[u8; 32]>::from(*self)[..])?;
Expand Down
231 changes: 214 additions & 17 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::{collections::HashSet, fmt::Debug, sync::Arc};
use chrono::Utc;
use futures::{stream::FuturesOrdered, FutureExt, StreamExt, TryFutureExt};
use hex::{FromHex, ToHex};
use hex_data::HexData;
use indexmap::IndexMap;
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
use jsonrpc_derive::rpc;
Expand All @@ -23,10 +24,11 @@ use zebra_chain::{
block::{self, Height, SerializedBlock},
chain_tip::{ChainTip, NetworkChainTipHeightEstimator},
parameters::{ConsensusBranchId, Network, NetworkUpgrade},
serialization::ZcashDeserialize,
serialization::{ZcashDeserialize, ZcashSerialize},
subtree::NoteCommitmentSubtreeIndex,
transaction::{self, SerializedTransaction, Transaction, UnminedTx},
transparent::{self, Address},
work::difficulty::ExpandedDifficulty,
};
use zebra_node_services::mempool;
use zebra_state::{HashOrHeight, MinedTx, OutputIndex, OutputLocation, TransactionLocation};
Expand Down Expand Up @@ -166,6 +168,23 @@ pub trait Rpc {
verbosity: Option<u8>,
) -> BoxFuture<Result<GetBlock>>;

/// Returns the requested block header by hash or height, as a [`GetBlockHeader`] JSON string.
///
/// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html)
/// method: post
/// tags: blockchain
///
/// # Parameters
///
/// - `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
#[rpc(name = "getblockheader")]
fn get_block_header(
&self,
hash_or_height: String,
verbose: Option<bool>,
) -> BoxFuture<Result<GetBlockHeader>>;

/// Returns the hash of the current best blockchain tip block, as a [`GetBlockHash`] JSON string.
///
/// zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html)
Expand Down Expand Up @@ -548,13 +567,11 @@ where
.await
.map_server_error()?;

let zebra_state::ReadResponse::BlockHeader(block_header) = response else {
let zebra_state::ReadResponse::BlockHeader { header, .. } = response else {
unreachable!("unmatched response to a BlockHeader request")
};

let tip_block_time = block_header
.ok_or_server_error("unexpectedly could not read best chain tip block header")?
.time;
let tip_block_time = header.time;

let now = Utc::now();
let zebra_estimated_height =
Expand Down Expand Up @@ -792,10 +809,6 @@ where
}
};

// TODO: look up the height if we only have a hash,
// this needs a new state request for the height -> hash index
let height = hash_or_height.height();

// # Concurrency
//
// We look up by block hash so the hash, transaction IDs, and confirmations
Expand Down Expand Up @@ -873,21 +886,18 @@ where
_ => unreachable!("unmatched response to a depth request"),
};

let time = if should_read_block_header {
let (time, height) = if should_read_block_header {
let block_header_response =
futs.next().await.expect("`futs` should not be empty");

match block_header_response.map_server_error()? {
zebra_state::ReadResponse::BlockHeader(header) => Some(
header
.ok_or_server_error("Block not found")?
.time
.timestamp(),
),
zebra_state::ReadResponse::BlockHeader { header, height, .. } => {
(Some(header.time.timestamp()), Some(height))
}
_ => unreachable!("unmatched response to a BlockHeader request"),
}
} else {
None
(None, hash_or_height.height())
};

let sapling = SaplingTrees {
Expand Down Expand Up @@ -919,6 +929,103 @@ where
.boxed()
}

fn get_block_header(
&self,
hash_or_height: String,
verbose: Option<bool>,
) -> BoxFuture<Result<GetBlockHeader>> {
let state = self.state.clone();
let verbose = verbose.unwrap_or(true);

async move {
let hash_or_height: HashOrHeight = hash_or_height.parse().map_server_error()?;
let zebra_state::ReadResponse::BlockHeader {
header,
hash,
height,
next_block_hash,
} = state
.clone()
.oneshot(zebra_state::ReadRequest::BlockHeader(hash_or_height))
.await
.map_server_error()?
else {
panic!("unexpected response to BlockHeader request")
};

let response = if !verbose {
GetBlockHeader::Raw(HexData(header.zcash_serialize_to_vec().map_server_error()?))
} else {
// TODO:
// - Return block hash and height in BlockHeader response
// - Add the block height to the `getblock` response as well
// - Add a parameter to the BlockHeader request to indicate that the caller is interested in the next block hash?
arya2 marked this conversation as resolved.
Show resolved Hide resolved

let zebra_state::ReadResponse::SaplingTree(sapling_tree) = state
.clone()
.oneshot(zebra_state::ReadRequest::SaplingTree(hash_or_height))
.await
.map_server_error()?
else {
panic!("unexpected response to SaplingTree request")
};

// TODO: Double-check that there's an empty Sapling root at Genesis.
let sapling_tree = sapling_tree.expect("should always have a sapling root");

let zebra_state::ReadResponse::Depth(depth) = state
.clone()
.oneshot(zebra_state::ReadRequest::Depth(hash))
.await
.map_server_error()?
else {
panic!("unexpected response to SaplingTree request")
};

// From <https://zcash.github.io/rpc/getblock.html>
// TODO: Deduplicate const definition, consider refactoring this to avoid duplicate logic
const NOT_IN_BEST_CHAIN_CONFIRMATIONS: i64 = -1;

// Confirmations are one more than the depth.
// Depth is limited by height, so it will never overflow an i64.
let confirmations = depth
.map(|depth| i64::from(depth) + 1)
.unwrap_or(NOT_IN_BEST_CHAIN_CONFIRMATIONS);

let mut nonce = *header.nonce;
nonce.reverse();

let mut final_sapling_root: [u8; 32] = sapling_tree.root().into();
final_sapling_root.reverse();

let block_header = GetBlockHeaderObject {
hash: GetBlockHash(hash),
confirmations,
height,
version: header.version,
merkle_root: header.merkle_root,
final_sapling_root,
time: header.time.timestamp(),
nonce,
bits: header.difficulty_threshold,
difficulty: header
.difficulty_threshold
.to_expanded()
.ok_or_server_error(
"could not convert compact difficulty to expanded difficulty",
)?,
previous_block_hash: GetBlockHash(header.previous_block_hash),
next_block_hash: next_block_hash.map(GetBlockHash),
};

GetBlockHeader::Object(Box::new(block_header))
};

Ok(response)
}
.boxed()
}

fn get_best_block_hash(&self) -> Result<GetBlockHash> {
self.latest_chain_tip
.best_tip_hash()
Expand Down Expand Up @@ -1623,6 +1730,96 @@ impl Default for GetBlock {
}
}

/// Response to a `getblockheader` RPC request.
///
/// See the notes for the [`Rpc::get_block_header`] method.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(untagged)]
pub enum GetBlockHeader {
/// The request block, hex-encoded.
arya2 marked this conversation as resolved.
Show resolved Hide resolved
Raw(hex_data::HexData),

/// The block object.
arya2 marked this conversation as resolved.
Show resolved Hide resolved
Object(Box<GetBlockHeaderObject>),
}

#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
/// Verbose response to a `getblockheader` RPC request.
///
/// See the notes for the [`Rpc::get_block_header`] method.
pub struct GetBlockHeaderObject {
/// The hash of the requested block.
pub hash: GetBlockHash,

/// The number of confirmations of this block in the best chain,
/// or -1 if it is not in the best chain.
pub confirmations: i64,

/// The height of the requested block.
pub height: Height,

/// The version field of the requested block.
pub version: u32,

/// The merkle root of the requesteed block.
#[serde(with = "hex", rename = "merkleroot")]
pub merkle_root: block::merkle::Root,

/// The root of the Sapling commitment tree after applying this block.
#[serde(with = "hex", rename = "finalsaplingroot")]
pub final_sapling_root: [u8; 32],

/// The block time of the requested block header in non-leap seconds since Jan 1 1970 GMT.
pub time: i64,

/// The nonce of the requested block header
#[serde(with = "hex")]
pub nonce: [u8; 32],

/// The difficulty threshold of the requested block header displayed in compact form.
#[serde(with = "hex")]
pub bits: zebra_chain::work::difficulty::CompactDifficulty,

/// The difficulty threshold of the requested block header displayed in expanded form.
#[serde(with = "hex")]
pub difficulty: zebra_chain::work::difficulty::ExpandedDifficulty,

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

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

impl Default for GetBlockHeader {
fn default() -> Self {
GetBlockHeader::Object(Box::default())
}
}

impl Default for GetBlockHeaderObject {
fn default() -> Self {
let difficulty: ExpandedDifficulty = zebra_chain::work::difficulty::U256::one().into();

GetBlockHeaderObject {
hash: GetBlockHash::default(),
confirmations: 0,
height: Height::MIN,
version: 4,
merkle_root: block::merkle::Root([0; 32]),
final_sapling_root: Default::default(),
time: 0,
nonce: [0; 32],
bits: difficulty.to_compact(),
difficulty,
previous_block_hash: Default::default(),
next_block_hash: Default::default(),
}
}
}

/// Response to a `getbestblockhash` and `getblockhash` RPC request.
///
/// Contains the hex-encoded hash of the requested block.
Expand Down
25 changes: 15 additions & 10 deletions zebra-rpc/src/methods/tests/prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,16 +663,21 @@ proptest! {
.expect_request(zebra_state::ReadRequest::BlockHeader(block_hash.into()))
.await
.expect("getblockchaininfo should call mock state service with correct request")
.respond(zebra_state::ReadResponse::BlockHeader(Some(Arc::new(block::Header {
time: block_time,
version: Default::default(),
previous_block_hash: Default::default(),
merkle_root: Default::default(),
commitment_bytes: Default::default(),
difficulty_threshold: Default::default(),
nonce: Default::default(),
solution: Default::default()
}))));
.respond(zebra_state::ReadResponse::BlockHeader {
header: Arc::new(block::Header {
time: block_time,
version: Default::default(),
previous_block_hash: Default::default(),
merkle_root: Default::default(),
commitment_bytes: Default::default(),
difficulty_threshold: Default::default(),
nonce: Default::default(),
solution: Default::default()
}),
hash: block::Hash::from([0; 32]),
height: Height::MIN,
next_block_hash: None,
});
}
};

Expand Down
Loading
Loading