change(log): Log a cute message for blocks that were mined by Zebra (off by default) (#6098)

* Mark Zebra coinbase transactions with extra coinbase data

* Log when we commit a block mined by Zebra to our state

* Reduce logging instrumentation during block writes

* Remove debug types in Zebra block log

* Add network and commit to write task logs

* Apply an allow-list before we log arbitrary user messages from blocks

* Rate-limit Zebra mined block logging to once every 1000 blocks

* Add mining configs for extra coinbase data and imitating zcashd, but don't use them yet

* Check CoinbaseData size limit when building transparent transactions

* Replace LIKE_ZCASHD constants with a config

* Take extra coinbase data from the configured string

* Update the zebrad configs in the tests with new config fields
This commit is contained in:
teor 2023-02-23 10:10:11 +10:00 committed by GitHub
parent a835270ff7
commit ec43d63ed2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 459 additions and 71 deletions

View File

@ -15,6 +15,7 @@ impl Transaction {
network: Network,
height: Height,
outputs: impl IntoIterator<Item = (Amount<NonNegative>, transparent::Script)>,
extra_coinbase_data: Vec<u8>,
) -> Transaction {
// # Consensus
//
@ -36,13 +37,17 @@ impl Transaction {
//
// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
//
// Zebra does not add any extra coinbase data.
// Zebra adds extra coinbase data if configured to do so.
//
// Since we're not using a lock time, any sequence number is valid here.
// See `Transaction::lock_time()` for the relevant consensus rules.
//
// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
let inputs = vec![transparent::Input::new_coinbase(height, None, None)];
let inputs = vec![transparent::Input::new_coinbase(
height,
Some(extra_coinbase_data),
None,
)];
// > The block subsidy is composed of a miner subsidy and a series of funding streams.
//
@ -108,17 +113,23 @@ impl Transaction {
height: Height,
outputs: impl IntoIterator<Item = (Amount<NonNegative>, transparent::Script)>,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> Transaction {
// `zcashd` includes an extra byte after the coinbase height in the coinbase data,
// and a sequence number of u32::MAX.
let mut extra_data = None;
let mut sequence = None;
// `zcashd` includes an extra byte after the coinbase height in the coinbase data,
// and a sequence number of u32::MAX.
if like_zcashd {
extra_data = Some(vec![0x00]);
sequence = Some(u32::MAX);
}
// Override like_zcashd if extra_coinbase_data was supplied
if !extra_coinbase_data.is_empty() {
extra_data = Some(extra_coinbase_data);
}
// # Consensus
//
// See the other consensus rules above in new_v5_coinbase().

View File

@ -1,5 +1,15 @@
//! Transparent-related (Bitcoin-inherited) functionality.
use std::{collections::HashMap, fmt, iter};
use crate::{
amount::{Amount, NonNegative},
block,
parameters::Network,
primitives::zcash_primitives,
transaction,
};
mod address;
mod keys;
mod opcodes;
@ -9,7 +19,7 @@ mod utxo;
pub use address::Address;
pub use script::Script;
pub use serialize::GENESIS_COINBASE_DATA;
pub use serialize::{GENESIS_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN};
pub use utxo::{
new_ordered_outputs, new_outputs, outputs_from_utxos, utxos_from_ordered_utxos,
CoinbaseSpendRestriction, OrderedUtxo, Utxo,
@ -20,24 +30,14 @@ pub use utxo::{
new_ordered_outputs_with_height, new_outputs_with_height, new_transaction_ordered_outputs,
};
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
#[cfg(any(test, feature = "proptest-impl"))]
mod arbitrary;
#[cfg(test)]
mod tests;
use crate::{
amount::{Amount, NonNegative},
block,
parameters::Network,
primitives::zcash_primitives,
transaction,
};
use std::{collections::HashMap, fmt, iter};
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
/// The maturity threshold for transparent coinbase outputs.
///
@ -48,16 +48,28 @@ use std::{collections::HashMap, fmt, iter};
/// [7.1](https://zips.z.cash/protocol/nu5.pdf#txnencodingandconsensus)
pub const MIN_TRANSPARENT_COINBASE_MATURITY: u32 = 100;
/// Extra coinbase data that identifies some coinbase transactions generated by Zebra.
/// <https://emojipedia.org/zebra/>
//
// # Note
//
// rust-analyzer will crash in some editors when moving over an actual Zebra emoji,
// so we encode it here. This is a known issue in emacs-lsp and other lsp implementations:
// - https://github.com/rust-lang/rust-analyzer/issues/9121
// - https://github.com/emacs-lsp/lsp-mode/issues/2080
// - https://github.com/rust-lang/rust-analyzer/issues/13709
pub const EXTRA_ZEBRA_COINBASE_DATA: &str = "z\u{1F993}";
/// Arbitrary data inserted by miners into a coinbase transaction.
//
// TODO: rename to ExtraCoinbaseData, because height is also part of the coinbase data?
#[derive(Clone, Eq, PartialEq)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Serialize))]
pub struct CoinbaseData(
/// Invariant: this vec, together with the coinbase height, must be less than
/// 100 bytes. We enforce this by only constructing CoinbaseData fields by
/// parsing blocks with 100-byte data fields. When we implement block
/// creation, we should provide a constructor for the coinbase data field
/// that restricts it to 95 = 100 -1 -4 bytes (safe for any block height up
/// to 500_000_000).
/// parsing blocks with 100-byte data fields, and checking newly created
/// CoinbaseData lengths in the transaction builder.
pub(super) Vec<u8>,
);
@ -182,17 +194,39 @@ impl fmt::Display for Input {
impl Input {
/// Returns a new coinbase input for `height` with optional `data` and `sequence`.
///
/// # Consensus
///
/// The combined serialized size of `height` and `data` can be at most 100 bytes.
///
/// > A coinbase transaction script MUST have length in {2 .. 100} bytes.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
///
/// # Panics
///
/// If the coinbase data is greater than [`MAX_COINBASE_DATA_LEN`].
#[cfg(feature = "getblocktemplate-rpcs")]
pub fn new_coinbase(
height: block::Height,
data: Option<Vec<u8>>,
sequence: Option<u32>,
) -> Input {
// "No extra coinbase data" is the default.
let data = data.unwrap_or_default();
let height_size = height.coinbase_zcash_serialized_size();
assert!(
data.len() + height_size <= MAX_COINBASE_DATA_LEN,
"invalid coinbase data: extra data {} bytes + height {height_size} bytes \
must be {} or less",
data.len(),
MAX_COINBASE_DATA_LEN,
);
Input::Coinbase {
height,
// "No extra coinbase data" is the default.
data: CoinbaseData(data.unwrap_or_default()),
data: CoinbaseData(data),
// If the caller does not specify the sequence number,
// use a sequence number that activates the LockTime.
@ -200,6 +234,14 @@ impl Input {
}
}
/// Returns the extra coinbase data in this input, if it is an [`Input::Coinbase`].
pub fn extra_coinbase_data(&self) -> Option<&CoinbaseData> {
match self {
Input::PrevOut { .. } => None,
Input::Coinbase { data, .. } => Some(data),
}
}
/// Returns the input's sequence number.
pub fn sequence(&self) -> u32 {
match self {

View File

@ -5,9 +5,9 @@ use std::io;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::{
block,
block::{self, Height},
serialization::{
zcash_serialize_bytes, ReadZcashExt, SerializationError, ZcashDeserialize,
zcash_serialize_bytes, FakeWriter, ReadZcashExt, SerializationError, ZcashDeserialize,
ZcashDeserializeInto, ZcashSerialize,
},
transaction,
@ -26,6 +26,16 @@ use super::{CoinbaseData, Input, OutPoint, Output, Script};
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
pub const MAX_COINBASE_DATA_LEN: usize = 100;
/// The maximum length of the encoded coinbase height.
///
/// # Consensus
///
/// > The length of heightBytes MUST be in the range {1 .. 5}. Then the encoding is the length
/// > of heightBytes encoded as one byte, followed by heightBytes itself.
///
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
pub const MAX_COINBASE_HEIGHT_DATA_LEN: usize = 6;
/// The minimum length of the coinbase data.
///
/// Includes the encoded coinbase height, if any.
@ -100,7 +110,6 @@ impl ZcashDeserialize for OutPoint {
pub(crate) fn parse_coinbase_height(
mut data: Vec<u8>,
) -> Result<(block::Height, CoinbaseData), SerializationError> {
use block::Height;
match (data.first(), data.len()) {
// Blocks 1 through 16 inclusive encode block height with OP_N opcodes.
(Some(op_n @ 0x51..=0x60), len) if len >= 1 => Ok((
@ -226,6 +235,17 @@ pub(crate) fn write_coinbase_height<W: io::Write>(
Ok(())
}
impl Height {
/// Get the size of `Height` when serialized into a coinbase input script.
pub fn coinbase_zcash_serialized_size(&self) -> usize {
let mut writer = FakeWriter(0);
let empty_data = CoinbaseData(Vec::new());
write_coinbase_height(*self, &empty_data, &mut writer).expect("writer should never fail");
writer.0
}
}
impl ZcashSerialize for Input {
/// Serialize this transparent input.
///

View File

@ -262,6 +262,10 @@ where
/// no matter what the estimated height or local clock is.
debug_force_finished_sync: bool,
/// Test-only option that makes RPC responses more like `zcashd`.
#[allow(dead_code)]
debug_like_zcashd: bool,
// Services
//
/// A handle to the mempool service.
@ -301,6 +305,7 @@ where
app_version: Version,
network: Network,
debug_force_finished_sync: bool,
debug_like_zcashd: bool,
mempool: Buffer<Mempool, mempool::Request>,
state: State,
latest_chain_tip: Tip,
@ -323,6 +328,7 @@ where
app_version,
network,
debug_force_finished_sync,
debug_like_zcashd,
mempool: mempool.clone(),
state: state.clone(),
latest_chain_tip: latest_chain_tip.clone(),
@ -763,14 +769,14 @@ where
use zebra_chain::block::MAX_BLOCK_BYTES;
#[cfg(feature = "getblocktemplate-rpcs")]
/// Determines whether the output of this RPC is sorted like zcashd
const SHOULD_USE_ZCASHD_ORDER: bool = true;
// Determines whether the output of this RPC is sorted like zcashd
let should_use_zcashd_order = self.debug_like_zcashd;
let mut mempool = self.mempool.clone();
async move {
#[cfg(feature = "getblocktemplate-rpcs")]
let request = if SHOULD_USE_ZCASHD_ORDER {
let request = if should_use_zcashd_order {
mempool::Request::FullTransactions
} else {
mempool::Request::TransactionIds

View File

@ -17,7 +17,9 @@ use zebra_chain::{
parameters::Network,
primitives,
serialization::ZcashDeserializeInto,
transparent,
transparent::{
self, EXTRA_ZEBRA_COINBASE_DATA, MAX_COINBASE_DATA_LEN, MAX_COINBASE_HEIGHT_DATA_LEN,
},
work::difficulty::{ExpandedDifficulty, U256},
};
use zebra_consensus::{
@ -246,6 +248,14 @@ where
/// Zebra currently only supports transparent addresses.
miner_address: Option<transparent::Address>,
/// Extra data to include in coinbase transaction inputs.
/// Limited to around 95 bytes by the consensus rules.
extra_coinbase_data: Vec<u8>,
/// Should Zebra's block templates try to imitate `zcashd`?
/// Developer-only config.
debug_like_zcashd: bool,
// Services
//
/// A handle to the mempool service.
@ -293,6 +303,10 @@ where
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
/// Create a new instance of the handler for getblocktemplate RPCs.
///
/// # Panics
///
/// If the `mining_config` is invalid.
#[allow(clippy::too_many_arguments)]
pub fn new(
network: Network,
@ -304,9 +318,38 @@ where
sync_status: SyncStatus,
address_book: AddressBook,
) -> Self {
// A limit on the configured extra coinbase data, regardless of the current block height.
// This is different from the consensus rule, which limits the total height + data.
const EXTRA_COINBASE_DATA_LIMIT: usize =
MAX_COINBASE_DATA_LEN - MAX_COINBASE_HEIGHT_DATA_LEN;
let debug_like_zcashd = mining_config.debug_like_zcashd;
// Hex-decode to bytes if possible, otherwise UTF-8 encode to bytes.
let extra_coinbase_data = mining_config.extra_coinbase_data.unwrap_or_else(|| {
if debug_like_zcashd {
""
} else {
EXTRA_ZEBRA_COINBASE_DATA
}
.to_string()
});
let extra_coinbase_data = hex::decode(&extra_coinbase_data)
.unwrap_or_else(|_error| extra_coinbase_data.as_bytes().to_vec());
assert!(
extra_coinbase_data.len() <= EXTRA_COINBASE_DATA_LIMIT,
"extra coinbase data is {} bytes, but Zebra's limit is {}.\n\
Configure mining.extra_coinbase_data with a shorter string",
extra_coinbase_data.len(),
EXTRA_COINBASE_DATA_LIMIT,
);
Self {
network,
miner_address: mining_config.miner_address,
extra_coinbase_data,
debug_like_zcashd,
mempool,
state,
latest_chain_tip,
@ -389,14 +432,11 @@ where
&self,
parameters: Option<get_block_template::JsonParameters>,
) -> BoxFuture<Result<get_block_template::Response>> {
// Should we generate coinbase transactions that are exactly like zcashd's?
//
// This is useful for testing, but either way Zebra should obey the consensus rules.
const COINBASE_LIKE_ZCASHD: bool = true;
// Clone Config
// Clone Configs
let network = self.network;
let miner_address = self.miner_address;
let debug_like_zcashd = self.debug_like_zcashd;
let extra_coinbase_data = self.extra_coinbase_data.clone();
// Clone Services
let mempool = self.mempool.clone();
@ -634,14 +674,13 @@ where
);
// Randomly select some mempool transactions.
//
// TODO: sort these transactions to match zcashd's order, to make testing easier.
let mempool_txs = zip317::select_mempool_transactions(
network,
next_block_height,
miner_address,
mempool_txs,
COINBASE_LIKE_ZCASHD,
debug_like_zcashd,
extra_coinbase_data.clone(),
)
.await;
@ -662,7 +701,8 @@ where
server_long_poll_id,
mempool_txs,
submit_old,
COINBASE_LIKE_ZCASHD,
debug_like_zcashd,
extra_coinbase_data,
);
Ok(response.into())

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use zebra_chain::transparent;
/// Mining configuration section.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
/// The address used for miner payouts.
@ -14,4 +14,28 @@ pub struct Config {
/// Zebra sends mining fees and miner rewards to this address in the
/// `getblocktemplate` RPC coinbase transaction.
pub miner_address: Option<transparent::Address>,
/// Extra data to include in coinbase transaction inputs.
/// Limited to around 95 bytes by the consensus rules.
///
/// If this string is hex-encoded, it will be hex-decoded into bytes.
/// Otherwise, it will be UTF-8 encoded into bytes.
pub extra_coinbase_data: Option<String>,
/// Should Zebra's block templates try to imitate `zcashd`?
///
/// This developer-only config is not supported for general use.
pub debug_like_zcashd: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
miner_address: None,
// For now, act like `zcashd` as much as possible.
// TODO: do we want to default to v5 transactions and Zebra coinbase data?
extra_coinbase_data: None,
debug_like_zcashd: true,
}
}
}

View File

@ -273,11 +273,18 @@ pub fn generate_coinbase_and_roots(
mempool_txs: &[VerifiedUnminedTx],
history_tree: Arc<zebra_chain::history_tree::HistoryTree>,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> (TransactionTemplate<NegativeOrZero>, DefaultRoots) {
// Generate the coinbase transaction
let miner_fee = calculate_miner_fee(mempool_txs);
let coinbase_txn =
generate_coinbase_transaction(network, height, miner_address, miner_fee, like_zcashd);
let coinbase_txn = generate_coinbase_transaction(
network,
height,
miner_address,
miner_fee,
like_zcashd,
extra_coinbase_data,
);
// Calculate block default roots
//
@ -301,13 +308,15 @@ pub fn generate_coinbase_transaction(
miner_address: transparent::Address,
miner_fee: Amount<NonNegative>,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> UnminedTx {
let outputs = standard_coinbase_outputs(network, height, miner_address, miner_fee, like_zcashd);
if like_zcashd {
Transaction::new_v4_coinbase(network, height, outputs, like_zcashd).into()
Transaction::new_v4_coinbase(network, height, outputs, like_zcashd, extra_coinbase_data)
.into()
} else {
Transaction::new_v5_coinbase(network, height, outputs).into()
Transaction::new_v5_coinbase(network, height, outputs, extra_coinbase_data).into()
}
}

View File

@ -182,6 +182,7 @@ impl GetBlockTemplate {
///
/// If `like_zcashd` is true, try to match the coinbase transactions generated by `zcashd`
/// in the `getblocktemplate` RPC.
#[allow(clippy::too_many_arguments)]
pub fn new(
network: Network,
miner_address: transparent::Address,
@ -190,6 +191,7 @@ impl GetBlockTemplate {
mempool_txs: Vec<VerifiedUnminedTx>,
submit_old: Option<bool>,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> Self {
// Calculate the next block height.
let next_block_height =
@ -229,6 +231,7 @@ impl GetBlockTemplate {
&mempool_txs,
chain_tip_and_local_time.history_tree.clone(),
like_zcashd,
extra_coinbase_data,
);
// Convert difficulty

View File

@ -46,11 +46,17 @@ pub async fn select_mempool_transactions(
miner_address: transparent::Address,
mempool_txs: Vec<VerifiedUnminedTx>,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> Vec<VerifiedUnminedTx> {
// Use a fake coinbase transaction to break the dependency between transaction
// selection, the miner fee, and the fee payment in the coinbase transaction.
let fake_coinbase_tx =
fake_coinbase_transaction(network, next_block_height, miner_address, like_zcashd);
let fake_coinbase_tx = fake_coinbase_transaction(
network,
next_block_height,
miner_address,
like_zcashd,
extra_coinbase_data,
);
// Setup the transaction lists.
let (mut conventional_fee_txs, mut low_fee_txs): (Vec<_>, Vec<_>) = mempool_txs
@ -117,6 +123,7 @@ pub fn fake_coinbase_transaction(
height: Height,
miner_address: transparent::Address,
like_zcashd: bool,
extra_coinbase_data: Vec<u8>,
) -> TransactionTemplate<NegativeOrZero> {
// Block heights are encoded as variable-length (script) and `u32` (lock time, expiry height).
// They can also change the `u32` consensus branch id.
@ -129,8 +136,14 @@ pub fn fake_coinbase_transaction(
// https://developer.bitcoin.org/reference/transactions.html#txout-a-transaction-output
let miner_fee = 1.try_into().expect("amount is valid and non-negative");
let coinbase_tx =
generate_coinbase_transaction(network, height, miner_address, miner_fee, like_zcashd);
let coinbase_tx = generate_coinbase_transaction(
network,
height,
miner_address,
miner_fee,
like_zcashd,
extra_coinbase_data,
);
TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee)
}

View File

@ -43,6 +43,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -96,6 +97,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -154,6 +156,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -220,6 +223,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -275,6 +279,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -328,6 +333,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -424,6 +430,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -481,6 +488,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -527,6 +535,7 @@ proptest! {
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -576,6 +585,7 @@ proptest! {
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
@ -661,6 +671,7 @@ proptest! {
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
@ -723,6 +734,7 @@ proptest! {
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
@ -773,6 +785,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -861,6 +874,7 @@ proptest! {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,

View File

@ -70,6 +70,7 @@ async fn test_rpc_response_data_for_network(network: Network) {
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,

View File

@ -98,6 +98,8 @@ pub async fn test_responses<State, ReadState>(
let mining_config = get_block_template_rpcs::config::Config {
miner_address: Some(transparent::Address::from_script_hash(network, [0xad; 20])),
extra_coinbase_data: None,
debug_like_zcashd: true,
};
// nu5 block height

View File

@ -32,6 +32,7 @@ async fn rpc_getinfo() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -75,6 +76,7 @@ async fn rpc_getblock() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
@ -225,6 +227,7 @@ async fn rpc_getblock_parse_error() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -266,6 +269,7 @@ async fn rpc_getblock_missing_error() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -333,6 +337,7 @@ async fn rpc_getbestblockhash() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
@ -374,6 +379,7 @@ async fn rpc_getrawtransaction() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
@ -461,6 +467,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
@ -603,6 +610,7 @@ async fn rpc_getaddresstxids_response_with(
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
@ -653,6 +661,7 @@ async fn rpc_getaddressutxos_invalid_arguments() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
@ -700,6 +709,7 @@ async fn rpc_getaddressutxos_response() {
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
@ -1116,7 +1126,11 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
true => Some(transparent::Address::from_pub_key_hash(Mainnet, [0x7e; 20])),
};
let mining_config = Config { miner_address };
let mining_config = Config {
miner_address,
extra_coinbase_data: None,
debug_like_zcashd: true,
};
// nu5 block height
let fake_tip_height = NetworkUpgrade::Nu5.activation_height(Mainnet).unwrap();
@ -1510,6 +1524,8 @@ async fn rpc_getdifficulty() {
let mining_config = Config {
miner_address: None,
extra_coinbase_data: None,
debug_like_zcashd: true,
};
// nu5 block height

View File

@ -145,7 +145,7 @@ impl RpcServer {
// Initialize the getblocktemplate rpc method handler
let get_block_template_rpc_impl = GetBlockTemplateRpcImpl::new(
network,
mining_config,
mining_config.clone(),
mempool.clone(),
state.clone(),
latest_chain_tip.clone(),
@ -162,6 +162,10 @@ impl RpcServer {
app_version.clone(),
network,
config.debug_force_finished_sync,
#[cfg(feature = "getblocktemplate-rpcs")]
mining_config.debug_like_zcashd,
#[cfg(not(feature = "getblocktemplate-rpcs"))]
true,
mempool,
state,
latest_chain_tip,

View File

@ -338,16 +338,19 @@ impl StateService {
tokio::sync::mpsc::unbounded_channel();
let finalized_state_for_writing = finalized_state.clone();
let span = Span::current();
let block_write_task = std::thread::spawn(move || {
write::write_blocks_from_channels(
finalized_block_write_receiver,
non_finalized_block_write_receiver,
finalized_state_for_writing,
non_finalized_state,
invalid_block_reset_sender,
chain_tip_sender,
non_finalized_state_sender,
)
span.in_scope(move || {
write::write_blocks_from_channels(
finalized_block_write_receiver,
non_finalized_block_write_receiver,
finalized_state_for_writing,
non_finalized_state,
invalid_block_reset_sender,
chain_tip_sender,
non_finalized_state_sender,
)
})
});
let block_write_task = Arc::new(block_write_task);

View File

@ -6,7 +6,10 @@ use tokio::sync::{
watch,
};
use zebra_chain::block::{self, Height};
use zebra_chain::{
block::{self, Height},
transparent::EXTRA_ZEBRA_COINBASE_DATA,
};
use crate::{
constants::MAX_BLOCK_REORG_HEIGHT,
@ -34,7 +37,15 @@ const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2;
/// Run contextual validation on the prepared block and add it to the
/// non-finalized state if it is contextually valid.
#[tracing::instrument(level = "debug", skip(prepared), fields(height = ?prepared.height, hash = %prepared.hash))]
#[tracing::instrument(
level = "debug",
skip(finalized_state, non_finalized_state, prepared),
fields(
height = ?prepared.height,
hash = %prepared.hash,
chains = non_finalized_state.chain_set.len()
)
)]
pub(crate) fn validate_and_commit_non_finalized(
finalized_state: &ZebraDb,
non_finalized_state: &mut NonFinalizedState,
@ -56,16 +67,28 @@ pub(crate) fn validate_and_commit_non_finalized(
/// channels with the latest non-finalized [`ChainTipBlock`] and
/// [`Chain`].
///
/// `last_zebra_mined_log_height` is used to rate-limit logging.
///
/// Returns the latest non-finalized chain tip height.
///
/// # Panics
///
/// If the `non_finalized_state` is empty.
#[instrument(level = "debug", skip(chain_tip_sender, non_finalized_state_sender))]
#[instrument(
level = "debug",
skip(
non_finalized_state,
chain_tip_sender,
non_finalized_state_sender,
last_zebra_mined_log_height
),
fields(chains = non_finalized_state.chain_set.len())
)]
fn update_latest_chain_channels(
non_finalized_state: &NonFinalizedState,
chain_tip_sender: &mut ChainTipSender,
non_finalized_state_sender: &watch::Sender<NonFinalizedState>,
last_zebra_mined_log_height: &mut Option<Height>,
) -> block::Height {
let best_chain = non_finalized_state.best_chain().expect("unexpected empty non-finalized state: must commit at least one block before updating channels");
@ -75,6 +98,8 @@ fn update_latest_chain_channels(
.clone();
let tip_block = ChainTipBlock::from(tip_block);
log_if_mined_by_zebra(&tip_block, last_zebra_mined_log_height);
let tip_block_height = tip_block.height;
// If the final receiver was just dropped, ignore the error.
@ -90,13 +115,21 @@ fn update_latest_chain_channels(
/// `non_finalized_state_sender`.
// TODO: make the task an object
#[allow(clippy::too_many_arguments)]
#[instrument(skip(
finalized_block_write_receiver,
non_finalized_block_write_receiver,
invalid_block_reset_sender,
chain_tip_sender,
non_finalized_state_sender,
))]
#[instrument(
level = "debug",
skip(
finalized_block_write_receiver,
non_finalized_block_write_receiver,
finalized_state,
non_finalized_state,
invalid_block_reset_sender,
chain_tip_sender,
non_finalized_state_sender,
),
fields(
network = %non_finalized_state.network
)
)]
pub fn write_blocks_from_channels(
mut finalized_block_write_receiver: UnboundedReceiver<QueuedFinalized>,
mut non_finalized_block_write_receiver: UnboundedReceiver<QueuedNonFinalized>,
@ -106,6 +139,8 @@ pub fn write_blocks_from_channels(
mut chain_tip_sender: ChainTipSender,
non_finalized_state_sender: watch::Sender<NonFinalizedState>,
) {
let mut last_zebra_mined_log_height = None;
// Write all the finalized blocks sent by the state,
// until the state closes the finalized block channel's sender.
while let Some(ordered_block) = finalized_block_write_receiver.blocking_recv() {
@ -147,6 +182,8 @@ pub fn write_blocks_from_channels(
Ok(finalized) => {
let tip_block = ChainTipBlock::from(finalized);
log_if_mined_by_zebra(&tip_block, &mut last_zebra_mined_log_height);
chain_tip_sender.set_finalized_tip(tip_block);
}
Err(error) => {
@ -243,6 +280,7 @@ pub fn write_blocks_from_channels(
&non_finalized_state,
&mut chain_tip_sender,
&non_finalized_state_sender,
&mut last_zebra_mined_log_height,
);
// Update the caller with the result.
@ -285,3 +323,72 @@ pub fn write_blocks_from_channels(
finalized_state.db.shutdown(true);
std::mem::drop(finalized_state);
}
/// Log a message if this block was mined by Zebra.
///
/// Does not detect early Zebra blocks, and blocks with custom coinbase transactions.
/// Rate-limited to every 1000 blocks using `last_zebra_mined_log_height`.
fn log_if_mined_by_zebra(
tip_block: &ChainTipBlock,
last_zebra_mined_log_height: &mut Option<Height>,
) {
// This logs at most every 2-3 checkpoints, which seems fine.
const LOG_RATE_LIMIT: u32 = 1000;
let height = tip_block.height.0;
if let Some(last_height) = last_zebra_mined_log_height {
if height < last_height.0 + LOG_RATE_LIMIT {
// If we logged in the last 1000 blocks, don't log anything now.
return;
}
};
// This code is rate-limited, so we can do expensive transformations here.
let coinbase_data = tip_block.transactions[0].inputs()[0]
.extra_coinbase_data()
.expect("valid blocks must start with a coinbase input")
.clone();
if coinbase_data
.as_ref()
.starts_with(EXTRA_ZEBRA_COINBASE_DATA.as_bytes())
{
let text = String::from_utf8_lossy(coinbase_data.as_ref());
*last_zebra_mined_log_height = Some(Height(height));
// No need for hex-encoded data if it's exactly what we expected.
if coinbase_data.as_ref() == EXTRA_ZEBRA_COINBASE_DATA.as_bytes() {
info!(
%text,
%height,
hash = %tip_block.hash,
"looks like this block was mined by Zebra!"
);
} else {
// # Security
//
// Use the extra data as an allow-list, replacing unknown characters.
// This makes sure control characters and harmful messages don't get logged
// to the terminal.
let text = text.replace(
|c: char| {
!EXTRA_ZEBRA_COINBASE_DATA
.to_ascii_lowercase()
.contains(c.to_ascii_lowercase())
},
"?",
);
let data = hex::encode(coinbase_data.as_ref());
info!(
%text,
%data,
%height,
hash = %tip_block.hash,
"looks like this block was mined by Zebra!"
);
}
}
}

View File

@ -0,0 +1,73 @@
# Default configuration for zebrad.
#
# This file can be used as a skeleton for custom configs.
#
# Unspecified fields use default values. Optional fields are Some(field) if the
# field is present and None if it is absent.
#
# This file is generated as an example using zebrad's current defaults.
# You should set only the config options you want to keep, and delete the rest.
# Only a subset of fields are present in the skeleton, since optional values
# whose default is None are omitted.
#
# The config format (including a complete list of sections and fields) is
# documented here:
# https://doc.zebra.zfnd.org/zebrad/config/struct.ZebradConfig.html
#
# zebrad attempts to load configs in the following order:
#
# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`;
# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent);
# 3. The default config.
[consensus]
checkpoint_sync = true
debug_skip_parameter_preload = false
[mempool]
eviction_memory_time = "1h"
tx_cost_limit = 80000000
[metrics]
[mining]
debug_like_zcashd = true
[network]
crawl_new_peer_interval = "1m 1s"
initial_mainnet_peers = [
"dnsseed.z.cash:8233",
"dnsseed.str4d.xyz:8233",
"mainnet.seeder.zfnd.org:8233",
"mainnet.is.yolo.money:8233",
]
initial_testnet_peers = [
"dnsseed.testnet.z.cash:18233",
"testnet.seeder.zfnd.org:18233",
"testnet.is.yolo.money:18233",
]
listen_addr = "0.0.0.0:8233"
network = "Mainnet"
peerset_initial_target_size = 25
[rpc]
debug_force_finished_sync = false
parallel_cpu_threads = 0
[state]
cache_dir = "cache_dir"
delete_old_database = true
ephemeral = false
[sync]
checkpoint_verify_concurrency_limit = 1000
download_concurrency_limit = 50
full_verify_concurrency_limit = 20
parallel_cpu_threads = 0
[tracing]
buffer_limit = 128000
force_use_color = false
use_color = true
use_journald = false