Skip to content

Commit

Permalink
change(rpc): add submitblock RPC method (#5526)
Browse files Browse the repository at this point in the history
* adds submitblock rpc method

* re-orders imports

* replaces thread::yield_now with async yield_now

* Fix doc warnings and unused variable warnings, add missing docs

* Mark work_id as optional

* Use the same ChainVerifier for downloaded and submitted blocks

* Revert unused changes & minor cleanups

* Document currently-unreachable code

* updates tests and submit_block response for AlreadyVerified error

* Update zebra-rpc/src/methods/get_block_template_rpcs.rs

Co-authored-by: Alfredo Garcia <[email protected]>

* changes names from BlockVerifier to ChainVerifier and block_verifier to chain_verifier to keep it consistent with naming in zebra-consensus

* move how to run the submit_block test example to acceptance.rs

* updates snapshot tests

* moved acceptance test to a separate file

* removes extra tower::ServiceBuilder::new(), updates docs

* updates vectors and snapshot tests, changes hex decoding error in submit_block method from server error to parse error

* hides errors module in zebra-rpc behind a feature flag and adds docs.

* Updates snapshot test, adds mod docs, moves HexData to its own mod, and removes the unrelated make_server_error refactor for now

* update submit block acceptance test mod doc

Co-authored-by: teor <[email protected]>
Co-authored-by: Alfredo Garcia <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Nov 4, 2022
1 parent 34313b8 commit 2f3b05f
Show file tree
Hide file tree
Showing 21 changed files with 638 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zebra-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"

[features]
default = []
getblocktemplate-rpcs = []
proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"]

[dependencies]
Expand Down
1 change: 1 addition & 0 deletions zebra-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ where
V: Service<tx::Request, Response = tx::Response, Error = BoxError> + Send + Clone + 'static,
V::Future: Send + 'static,
{
/// Creates a new BlockVerifier
pub fn new(network: Network, state_service: S, transaction_verifier: V) -> Self {
Self {
network,
Expand Down
1 change: 1 addition & 0 deletions zebra-consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub mod chain;
pub mod error;

pub use block::VerifyBlockError;
pub use chain::VerifyChainError;
pub use checkpoint::{
CheckpointList, VerifyCheckpointError, MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP,
};
Expand Down
4 changes: 3 additions & 1 deletion zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ edition = "2021"

[features]
default = []
getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs", "zebra-node-services/getblocktemplate-rpcs"]
getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs", "zebra-node-services/getblocktemplate-rpcs", "zebra-consensus/getblocktemplate-rpcs"]

# Test-only features
proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"]
Expand Down Expand Up @@ -43,6 +43,7 @@ proptest = { version = "0.10.1", optional = true }
proptest-derive = { version = "0.3.0", optional = true }

zebra-chain = { path = "../zebra-chain" }
zebra-consensus = { path = "../zebra-consensus" }
zebra-network = { path = "../zebra-network" }
zebra-node-services = { path = "../zebra-node-services" }
zebra-state = { path = "../zebra-state" }
Expand All @@ -57,5 +58,6 @@ thiserror = "1.0.37"
tokio = { version = "1.21.2", features = ["full", "tracing", "test-util"] }

zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
zebra-consensus = { path = "../zebra-consensus" }
zebra-state = { path = "../zebra-state", features = ["proptest-impl"] }
zebra-test = { path = "../zebra-test/" }
134 changes: 127 additions & 7 deletions zebra-rpc/src/methods/get_block_template_rpcs.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
//! RPC methods related to mining only available with `getblocktemplate-rpcs` rust feature.
use std::sync::Arc;

use futures::{FutureExt, TryFutureExt};
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
use jsonrpc_derive::rpc;
use tower::{buffer::Buffer, Service, ServiceExt};

use zebra_chain::{amount::Amount, block::Height, chain_tip::ChainTip};
use zebra_chain::{
amount::Amount,
block::Height,
block::{self, Block},
chain_tip::ChainTip,
serialization::ZcashDeserializeInto,
};
use zebra_consensus::{BlockError, VerifyBlockError, VerifyChainError, VerifyCheckpointError};
use zebra_node_services::mempool;

use crate::methods::{
get_block_template_rpcs::types::{
default_roots::DefaultRoots, get_block_template::GetBlockTemplate,
transaction::TransactionTemplate,
default_roots::DefaultRoots, get_block_template::GetBlockTemplate, hex_data::HexData,
submit_block, transaction::TransactionTemplate,
},
GetBlockHash, MISSING_BLOCK_ERROR_CODE,
};
Expand Down Expand Up @@ -74,10 +83,26 @@ pub trait GetBlockTemplateRpc {
/// This rpc method is available only if zebra is built with `--features getblocktemplate-rpcs`.
#[rpc(name = "getblocktemplate")]
fn get_block_template(&self) -> BoxFuture<Result<GetBlockTemplate>>;

/// Submits block to the node to be validated and committed.
/// Returns the [`submit_block::Response`] for the operation, as a JSON string.
///
/// zcashd reference: [`submitblock`](https://zcash.github.io/rpc/submitblock.html)
///
/// # Parameters
/// - `hexdata` (string, required)
/// - `jsonparametersobject` (string, optional) - currently ignored
/// - holds a single field, workid, that must be included in submissions if provided by the server.
#[rpc(name = "submitblock")]
fn submit_block(
&self,
hex_data: HexData,
_options: Option<submit_block::JsonParameters>,
) -> BoxFuture<Result<submit_block::Response>>;
}

/// RPC method implementations.
pub struct GetBlockTemplateRpcImpl<Mempool, State, Tip>
pub struct GetBlockTemplateRpcImpl<Mempool, State, Tip, ChainVerifier>
where
Mempool: Service<
mempool::Request,
Expand All @@ -89,7 +114,11 @@ where
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
>,
Tip: ChainTip,
ChainVerifier: Service<Arc<Block>, Response = block::Hash, Error = zebra_consensus::BoxError>
+ Clone
+ Send
+ Sync
+ 'static,
{
// TODO: Add the other fields from the [`Rpc`] struct as-needed

Expand All @@ -107,9 +136,12 @@ where

/// Allows efficient access to the best tip of the blockchain.
latest_chain_tip: Tip,

/// The chain verifier, used for submitting blocks.
chain_verifier: ChainVerifier,
}

impl<Mempool, State, Tip> GetBlockTemplateRpcImpl<Mempool, State, Tip>
impl<Mempool, State, Tip, ChainVerifier> GetBlockTemplateRpcImpl<Mempool, State, Tip, ChainVerifier>
where
Mempool: Service<
mempool::Request,
Expand All @@ -125,22 +157,30 @@ where
+ Sync
+ 'static,
Tip: ChainTip + Clone + Send + Sync + 'static,
ChainVerifier: Service<Arc<Block>, Response = block::Hash, Error = zebra_consensus::BoxError>
+ Clone
+ Send
+ Sync
+ 'static,
{
/// Create a new instance of the handler for getblocktemplate RPCs.
pub fn new(
mempool: Buffer<Mempool, mempool::Request>,
state: State,
latest_chain_tip: Tip,
chain_verifier: ChainVerifier,
) -> Self {
Self {
mempool,
state,
latest_chain_tip,
chain_verifier,
}
}
}

impl<Mempool, State, Tip> GetBlockTemplateRpc for GetBlockTemplateRpcImpl<Mempool, State, Tip>
impl<Mempool, State, Tip, ChainVerifier> GetBlockTemplateRpc
for GetBlockTemplateRpcImpl<Mempool, State, Tip, ChainVerifier>
where
Mempool: Service<
mempool::Request,
Expand All @@ -158,6 +198,12 @@ where
+ 'static,
<State as Service<zebra_state::ReadRequest>>::Future: Send,
Tip: ChainTip + Send + Sync + 'static,
ChainVerifier: Service<Arc<Block>, Response = block::Hash, Error = zebra_consensus::BoxError>
+ Clone
+ Send
+ Sync
+ 'static,
<ChainVerifier as Service<Arc<Block>>>::Future: Send,
{
fn get_block_count(&self) -> Result<u32> {
self.latest_chain_tip
Expand Down Expand Up @@ -302,6 +348,80 @@ where
}
.boxed()
}

fn submit_block(
&self,
HexData(block_bytes): HexData,
_options: Option<submit_block::JsonParameters>,
) -> BoxFuture<Result<submit_block::Response>> {
let mut chain_verifier = self.chain_verifier.clone();

async move {
let block: Block = match block_bytes.zcash_deserialize_into() {
Ok(block_bytes) => block_bytes,
Err(_) => return Ok(submit_block::ErrorResponse::Rejected.into()),
};

let chain_verifier_response = chain_verifier
.ready()
.await
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?
.call(Arc::new(block))
.await;

let chain_error = match chain_verifier_response {
// Currently, this match arm returns `null` (Accepted) for blocks committed
// to any chain, but Accepted is only for blocks in the best chain.
//
// TODO (#5487):
// - Inconclusive: check if the block is on a side-chain
// The difference is important to miners, because they want to mine on the best chain.
Ok(_block_hash) => return Ok(submit_block::Response::Accepted),

// Turns BoxError into Result<VerifyChainError, BoxError>,
// by downcasting from Any to VerifyChainError.
Err(box_error) => box_error
.downcast::<VerifyChainError>()
.map(|boxed_chain_error| *boxed_chain_error),
};

Ok(match chain_error {
Ok(
VerifyChainError::Checkpoint(VerifyCheckpointError::AlreadyVerified { .. })
| VerifyChainError::Block(VerifyBlockError::Block {
source: BlockError::AlreadyInChain(..),
}),
) => submit_block::ErrorResponse::Duplicate,

// Currently, these match arms return Reject for the older duplicate in a queue,
// but queued duplicates should be DuplicateInconclusive.
//
// Optional TODO (#5487):
// - DuplicateInconclusive: turn these non-finalized state duplicate block errors
// into BlockError enum variants, and handle them as DuplicateInconclusive:
// - "block already sent to be committed to the state"
// - "replaced by newer request"
// - keep the older request in the queue,
// and return a duplicate error for the newer request immediately.
// This improves the speed of the RPC response.
//
// Checking the download queues and ChainVerifier buffer for duplicates
// might require architectural changes to Zebra, so we should only do it
// if mining pools really need it.
Ok(_verify_chain_error) => submit_block::ErrorResponse::Rejected,

// This match arm is currently unreachable, but if future changes add extra error types,
// we want to turn them into `Rejected`.
Err(_unknown_error_type) => submit_block::ErrorResponse::Rejected,
}
.into())
}
.boxed()
}
}

/// Given a potentially negative index, find the corresponding `Height`.
Expand Down
2 changes: 2 additions & 0 deletions zebra-rpc/src/methods/get_block_template_rpcs/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
pub(crate) mod default_roots;
pub(crate) mod get_block_template;
pub(crate) mod hex_data;
pub(crate) mod submit_block;
pub(crate) mod transaction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Deserializes hex-encoded inputs such as the one required
//! for the `submitblock` RPC method.
/// Deserialize hex-encoded strings to bytes.
#[derive(Debug, PartialEq, Eq, serde::Deserialize)]
pub struct HexData(#[serde(with = "hex")] pub Vec<u8>);
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Parameter and response types for the `submitblock` RPC.
// Allow doc links to these imports.
#[allow(unused_imports)]
use crate::methods::get_block_template_rpcs::GetBlockTemplateRpc;

/// Optional argument `jsonparametersobject` for `submitblock` RPC request
///
/// See notes for [`GetBlockTemplateRpc::submit_block`] method
#[derive(Debug, serde::Deserialize)]
pub struct JsonParameters {
pub(crate) _work_id: Option<String>,
}

/// Response to a `submitblock` RPC request.
///
/// Zebra never returns "duplicate-invalid", because it does not store invalid blocks.
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum ErrorResponse {
/// Block was already committed to the non-finalized or finalized state
Duplicate,
/// Block was already added to the state queue or channel, but not yet committed to the non-finalized state
DuplicateInconclusive,
/// Block was already committed to the non-finalized state, but not on the best chain
Inconclusive,
/// Block rejected as invalid
Rejected,
}

/// Response to a `submitblock` RPC request.
///
/// Zebra never returns "duplicate-invalid", because it does not store invalid blocks.
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
#[serde(untagged)]
pub enum Response {
/// Block was not successfully submitted, return error
ErrorResponse(ErrorResponse),
/// Block successfully submitted, returns null
Accepted,
}

impl From<ErrorResponse> for Response {
fn from(error_response: ErrorResponse) -> Self {
Self::ErrorResponse(error_response)
}
}
5 changes: 4 additions & 1 deletion zebra-rpc/src/methods/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ async fn test_rpc_response_data_for_network(network: Network) {
let mut mempool: MockService<_, _, _, zebra_node_services::BoxError> =
MockService::build().for_unit_tests();
// Create a populated state service
let (_state, read_state, latest_chain_tip, _chain_tip_change) =
#[cfg_attr(not(feature = "getblocktemplate-rpcs"), allow(unused_variables))]
let (state, read_state, latest_chain_tip, _chain_tip_change) =
zebra_state::populated_state(blocks.clone(), network).await;

// Start snapshots of RPC responses.
Expand All @@ -56,7 +57,9 @@ async fn test_rpc_response_data_for_network(network: Network) {
// Test getblocktemplate-rpcs snapshots
#[cfg(feature = "getblocktemplate-rpcs")]
get_block_template_rpcs::test_responses(
network,
mempool.clone(),
state,
read_state.clone(),
latest_chain_tip.clone(),
settings.clone(),
Expand Down
Loading

0 comments on commit 2f3b05f

Please sign in to comment.