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

5. change(rpc): Implement coinbase conversion to RPC TransactionTemplate type #5554

Merged
merged 11 commits into from
Nov 7, 2022
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.

2 changes: 1 addition & 1 deletion zebra-chain/src/value_balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ where
}

impl ValueBalance<NegativeAllowed> {
/// Assumes that this value balance is a transaction value balance,
/// Assumes that this value balance is a non-coinbase transaction value balance,
/// and returns the remaining value in the transaction value pool.
///
/// # Consensus
Expand Down
5 changes: 2 additions & 3 deletions zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,17 @@ zebra-chain = { path = "../zebra-chain" }
zebra-consensus = { path = "../zebra-consensus" }
zebra-network = { path = "../zebra-network" }
zebra-node-services = { path = "../zebra-node-services" }
zebra-script = { path = "../zebra-script" }
zebra-state = { path = "../zebra-state" }

[dev-dependencies]
insta = { version = "1.21.0", features = ["redactions", "json"] }
proptest = "0.10.1"
proptest-derive = "0.3.0"
serde_json = "1.0.87"
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" }
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
zebra-state = { path = "../zebra-state", features = ["proptest-impl"] }
zebra-test = { path = "../zebra-test/" }
zebra-test = { path = "../zebra-test" }
180 changes: 132 additions & 48 deletions zebra-rpc/src/methods/get_block_template_rpcs.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
//! RPC methods related to mining only available with `getblocktemplate-rpcs` rust feature.

use std::sync::Arc;
use std::{iter, 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,
amount::{self, Amount, NonNegative},
block::Height,
block::{self, Block},
block::{
self,
merkle::{self, AuthDataRoot},
Block,
},
chain_tip::ChainTip,
parameters::Network,
serialization::ZcashDeserializeInto,
transaction::{UnminedTx, VerifiedUnminedTx},
};
use zebra_consensus::{BlockError, VerifyBlockError, VerifyChainError, VerifyCheckpointError};
use zebra_node_services::mempool;
Expand Down Expand Up @@ -126,6 +132,9 @@ where
// Configuration
//
// TODO: add mining config for getblocktemplate RPC miner address
//
/// The configured network for this RPC service.
_network: Network,

// Services
//
Expand Down Expand Up @@ -166,12 +175,14 @@ where
{
/// Create a new instance of the handler for getblocktemplate RPCs.
pub fn new(
network: Network,
mempool: Buffer<Mempool, mempool::Request>,
state: State,
latest_chain_tip: Tip,
chain_verifier: ChainVerifier,
) -> Self {
Self {
_network: network,
mempool,
state,
latest_chain_tip,
Expand Down Expand Up @@ -250,35 +261,63 @@ where
// Since this is a very large RPC, we use separate functions for each group of fields.
async move {
let _tip_height = best_chain_tip_height(&latest_chain_tip)?;
let mempool_txs = select_mempool_transactions(mempool).await?;

// TODO: put this in a separate get_mempool_transactions() function
let request = mempool::Request::FullTransactions;
let response = mempool.oneshot(request).await.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;

let transactions = if let mempool::Response::FullTransactions(transactions) = response {
// TODO: select transactions according to ZIP-317 (#5473)
transactions
} else {
unreachable!("unmatched response to a mempool::FullTransactions request");
};
let miner_fee = miner_fee(&mempool_txs);

/*
Fake a "coinbase" transaction by duplicating a mempool transaction,
or fake a response.

let merkle_root;
let auth_data_root;
This is temporarily required for the tests to pass.

// TODO: add the coinbase transaction to these lists, and delete the is_empty() check
if !transactions.is_empty() {
merkle_root = transactions.iter().cloned().collect();
auth_data_root = transactions.iter().cloned().collect();
TODO: create a method Transaction::new_v5_coinbase(network, tip_height, miner_fee),
and call it here.
*/
let coinbase_tx = if mempool_txs.is_empty() {
let empty_string = String::from("");
return Ok(GetBlockTemplate {
capabilities: vec![],
version: 0,
previous_block_hash: GetBlockHash([0; 32].into()),
block_commitments_hash: [0; 32].into(),
light_client_root_hash: [0; 32].into(),
final_sapling_root_hash: [0; 32].into(),
default_roots: DefaultRoots {
merkle_root: [0; 32].into(),
chain_history_root: [0; 32].into(),
auth_data_root: [0; 32].into(),
block_commitments_hash: [0; 32].into(),
},
transactions: Vec::new(),
coinbase_txn: TransactionTemplate {
data: Vec::new().into(),
hash: [0; 32].into(),
auth_digest: [0; 32].into(),
depends: Vec::new(),
fee: Amount::zero(),
sigops: 0,
required: true,
},
target: empty_string.clone(),
min_time: 0,
mutable: vec![],
nonce_range: empty_string.clone(),
sigop_limit: 0,
size_limit: 0,
cur_time: 0,
bits: empty_string,
height: 0,
});
} else {
merkle_root = [0; 32].into();
auth_data_root = [0; 32].into();
}
mempool_txs[0].transaction.clone()
};

let (merkle_root, auth_data_root) =
calculate_transaction_roots(&coinbase_tx, &mempool_txs);

let transactions = transactions.iter().map(Into::into).collect();
// Convert into TransactionTemplates
let mempool_txs = mempool_txs.iter().map(Into::into).collect();

let empty_string = String::from("");
Ok(GetBlockTemplate {
Expand All @@ -297,28 +336,9 @@ where
block_commitments_hash: [0; 32].into(),
},

transactions,
transactions: mempool_txs,

// TODO: move to a separate function in the transactions module
coinbase_txn: TransactionTemplate {
// TODO: generate coinbase transaction data
data: vec![].into(),

// TODO: calculate from transaction data
hash: [0; 32].into(),
auth_digest: [0; 32].into(),

// Always empty for coinbase transactions.
depends: Vec::new(),

// TODO: negative sum of transactions.*.fee
fee: Amount::zero(),

// TODO: sigops used by the generated transaction data
sigops: 0,

required: true,
},
coinbase_txn: TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee),

target: empty_string.clone(),

Expand Down Expand Up @@ -416,6 +436,70 @@ where
}
}

// get_block_template support methods

/// Returns selected transactions in the `mempool`, or an error if the mempool has failed.
///
/// TODO: select transactions according to ZIP-317 (#5473)
pub async fn select_mempool_transactions<Mempool>(
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
mempool: Mempool,
) -> Result<Vec<VerifiedUnminedTx>>
where
Mempool: Service<
mempool::Request,
Response = mempool::Response,
Error = zebra_node_services::BoxError,
> + 'static,
Mempool::Future: Send,
{
let response = mempool
.oneshot(mempool::Request::FullTransactions)
.await
.map_err(|error| Error {
code: ErrorCode::ServerError(0),
message: error.to_string(),
data: None,
})?;

if let mempool::Response::FullTransactions(transactions) = response {
// TODO: select transactions according to ZIP-317 (#5473)
Ok(transactions)
} else {
unreachable!("unmatched response to a mempool::FullTransactions request");
}
}

/// Returns the total miner fee for `mempool_txs`.
pub fn miner_fee(mempool_txs: &[VerifiedUnminedTx]) -> Amount<NonNegative> {
let miner_fee: amount::Result<Amount<NonNegative>> =
mempool_txs.iter().map(|tx| tx.miner_fee).sum();

miner_fee.expect(
"invalid selected transactions: \
fees in a valid block can not be more than MAX_MONEY",
)
}

/// Returns the transaction effecting and authorizing roots
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
/// for `coinbase_tx` and `mempool_txs`.
//
// TODO: should this be spawned into a cryptographic operations pool?
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
// (it would only matter if there were a lot of small transactions in a block)
pub fn calculate_transaction_roots(
coinbase_tx: &UnminedTx,
mempool_txs: &[VerifiedUnminedTx],
) -> (merkle::Root, AuthDataRoot) {
let block_transactions =
|| iter::once(coinbase_tx).chain(mempool_txs.iter().map(|tx| &tx.transaction));

let merkle_root = block_transactions().cloned().collect();
let auth_data_root = block_transactions().cloned().collect();

(merkle_root, auth_data_root)
}

// get_block_hash support methods

/// Given a potentially negative index, find the corresponding `Height`.
///
/// This function is used to parse the integer index argument of `get_block_hash`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
//! The `TransactionTemplate` type is part of the `getblocktemplate` RPC method output.

use zebra_chain::{
amount::{self, Amount, NonNegative},
amount::{self, Amount, NegativeOrZero, NonNegative},
block::merkle::AUTH_DIGEST_PLACEHOLDER,
transaction::{self, SerializedTransaction, VerifiedUnminedTx},
transaction::{self, SerializedTransaction, UnminedTx, VerifiedUnminedTx},
};
use zebra_script::CachedFfiTransaction;

/// Transaction data and fields needed to generate blocks using the `getblocktemplate` RPC.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -50,9 +51,14 @@ where
pub(crate) required: bool,
}

// Convert from a mempool transaction to a transaction template.
// Convert from a mempool transaction to a non-coinbase transaction template.
impl From<&VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
fn from(tx: &VerifiedUnminedTx) -> Self {
assert!(
!tx.transaction.transaction.is_coinbase(),
"unexpected coinbase transaction in mempool"
);

Self {
data: tx.transaction.transaction.as_ref().into(),
hash: tx.transaction.id.mined_id(),
Expand Down Expand Up @@ -80,3 +86,45 @@ impl From<VerifiedUnminedTx> for TransactionTemplate<NonNegative> {
Self::from(&tx)
}
}

impl TransactionTemplate<NegativeOrZero> {
/// Convert from a generated coinbase transaction into a coinbase transaction template.
///
/// `miner_fee` is the total miner fees for the block, excluding newly created block rewards.
//
// TODO: use a different type for generated coinbase transactions?
pub fn from_coinbase(tx: &UnminedTx, miner_fee: Amount<NonNegative>) -> Self {
assert!(
tx.transaction.is_coinbase(),
"invalid generated coinbase transaction: \
must have exactly one input, which must be a coinbase input",
);

let miner_fee = miner_fee
.constrain()
.expect("negating a NonNegative amount always results in a valid NegativeOrZero");

let legacy_sigop_count = CachedFfiTransaction::new(tx.transaction.clone(), Vec::new())
.legacy_sigop_count()
.expect(
"invalid generated coinbase transaction: \
failure in zcash_script sigop count",
);

Self {
data: tx.transaction.as_ref().into(),
hash: tx.id.mined_id(),
auth_digest: tx.id.auth_digest().unwrap_or(AUTH_DIGEST_PLACEHOLDER),

// Always empty, coinbase transactions never have inputs.
depends: Vec::new(),

fee: miner_fee,

sigops: legacy_sigop_count,

// Zcash requires a coinbase transaction.
required: true,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub async fn test_responses<State, ReadState>(
.await;

let get_block_template_rpc = GetBlockTemplateRpcImpl::new(
network,
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
Expand Down
Loading