zebra/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs

628 lines
22 KiB
Rust

//! Snapshot tests for getblocktemplate RPCs.
//!
//! To update these snapshots, run:
//! ```sh
//! cargo insta test --review --features getblocktemplate-rpcs --delete-unreferenced-snapshots
//! ```
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
time::Instant,
};
use hex::FromHex;
use insta::Settings;
use jsonrpc_core::Result;
use tower::{buffer::Buffer, Service};
use zebra_chain::{
block::Hash,
chain_sync_status::MockSyncStatus,
chain_tip::mock::MockChainTip,
parameters::{Network, NetworkUpgrade},
serialization::{DateTime32, ZcashDeserializeInto},
transaction::Transaction,
transparent,
work::difficulty::{CompactDifficulty, ExpandedDifficulty},
};
use zebra_network::{address_book_peers::MockAddressBookPeers, types::MetaAddr};
use zebra_node_services::mempool;
use zebra_state::{GetBlockTemplateChainInfo, ReadRequest, ReadResponse};
use zebra_test::{
mock_service::{MockService, PanicAssertion},
vectors::BLOCK_MAINNET_1_BYTES,
};
use crate::methods::{
get_block_template_rpcs::types::{
get_block_template::{self, GetBlockTemplateRequestMode},
get_mining_info,
hex_data::HexData,
long_poll::{LongPollId, LONG_POLL_ID_LENGTH},
peer_info::PeerInfo,
submit_block,
subsidy::BlockSubsidy,
unified_address, validate_address, z_validate_address,
},
tests::{snapshot::EXCESSIVE_BLOCK_HEIGHT, utils::fake_history_tree},
GetBlockHash, GetBlockTemplateRpc, GetBlockTemplateRpcImpl,
};
pub async fn test_responses<State, ReadState>(
network: Network,
mempool: MockService<
mempool::Request,
mempool::Response,
PanicAssertion,
zebra_node_services::BoxError,
>,
state: State,
read_state: ReadState,
settings: Settings,
) where
State: Service<
zebra_state::Request,
Response = zebra_state::Response,
Error = zebra_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zebra_state::Request>>::Future: Send,
ReadState: Service<
zebra_state::ReadRequest,
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<ReadState as Service<zebra_state::ReadRequest>>::Future: Send,
{
let (
block_verifier_router,
_transaction_verifier,
_parameter_download_task_handle,
_max_checkpoint_height,
) = zebra_consensus::router::init(zebra_consensus::Config::default(), network, state.clone())
.await;
let mut mock_sync_status = MockSyncStatus::default();
mock_sync_status.set_is_close_to_tip(true);
#[allow(clippy::unnecessary_struct_initialization)]
let mining_config = crate::config::mining::Config {
miner_address: Some(transparent::Address::from_script_hash(network, [0xad; 20])),
extra_coinbase_data: None,
debug_like_zcashd: true,
// TODO: Use default field values when optional features are enabled in tests #8183
//..Default::default()
};
// nu5 block height
let fake_tip_height = NetworkUpgrade::Nu5.activation_height(network).unwrap();
// nu5 block hash
let fake_tip_hash =
Hash::from_hex("0000000000d723156d9b65ffcf4984da7a19675ed7e2f06d9e5d5188af087bf8").unwrap();
// nu5 block time + 1
let fake_min_time = DateTime32::from(1654008606);
// nu5 block time + 12
let fake_cur_time = DateTime32::from(1654008617);
// nu5 block time + 123
let fake_max_time = DateTime32::from(1654008728);
// Use a valid fractional difficulty for snapshots
let pow_limit = ExpandedDifficulty::target_difficulty_limit(network);
let fake_difficulty = pow_limit * 2 / 3;
let fake_difficulty = CompactDifficulty::from(fake_difficulty);
let (mock_chain_tip, mock_chain_tip_sender) = MockChainTip::new();
mock_chain_tip_sender.send_best_tip_height(fake_tip_height);
mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash);
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
let mock_address_book = MockAddressBookPeers::new(vec![MetaAddr::new_initial_peer(
SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
network.default_port(),
)
.into(),
)
.into_new_meta_addr(Instant::now(), DateTime32::now())]);
// get an rpc instance with continuous blockchain state
let get_block_template_rpc = GetBlockTemplateRpcImpl::new(
network,
mining_config.clone(),
Buffer::new(mempool.clone(), 1),
read_state,
mock_chain_tip.clone(),
block_verifier_router.clone(),
mock_sync_status.clone(),
mock_address_book,
);
// `getblockcount`
let get_block_count = get_block_template_rpc
.get_block_count()
.expect("We should have a number");
snapshot_rpc_getblockcount(get_block_count, &settings);
// `getblockhash`
const BLOCK_HEIGHT10: i32 = 10;
let get_block_hash = get_block_template_rpc
.get_block_hash(BLOCK_HEIGHT10)
.await
.expect("We should have a GetBlockHash struct");
snapshot_rpc_getblockhash_valid(get_block_hash, &settings);
let get_block_hash = get_block_template_rpc
.get_block_hash(
EXCESSIVE_BLOCK_HEIGHT
.try_into()
.expect("constant fits in i32"),
)
.await;
snapshot_rpc_getblockhash_invalid("excessive_height", get_block_hash, &settings);
// `getmininginfo`
let get_mining_info = get_block_template_rpc
.get_mining_info()
.await
.expect("We should have a success response");
snapshot_rpc_getmininginfo(get_mining_info, &settings);
// `getblocksubsidy`
let fake_future_block_height = fake_tip_height.0 + 100_000;
let get_block_subsidy = get_block_template_rpc
.get_block_subsidy(Some(fake_future_block_height))
.await
.expect("We should have a success response");
snapshot_rpc_getblocksubsidy("future_height", get_block_subsidy, &settings);
let get_block_subsidy = get_block_template_rpc
.get_block_subsidy(None)
.await
.expect("We should have a success response");
snapshot_rpc_getblocksubsidy("tip_height", get_block_subsidy, &settings);
let get_block_subsidy = get_block_template_rpc
.get_block_subsidy(Some(EXCESSIVE_BLOCK_HEIGHT))
.await
.expect("We should have a success response");
snapshot_rpc_getblocksubsidy("excessive_height", get_block_subsidy, &settings);
// `getpeerinfo`
let get_peer_info = get_block_template_rpc
.get_peer_info()
.await
.expect("We should have a success response");
snapshot_rpc_getpeerinfo(get_peer_info, &settings);
// `getnetworksolps` (and `getnetworkhashps`)
//
// TODO: add tests for excessive num_blocks and height (#6688)
// add the same tests for get_network_hash_ps
let get_network_sol_ps = get_block_template_rpc
.get_network_sol_ps(None, None)
.await
.expect("We should have a success response");
snapshot_rpc_getnetworksolps(get_network_sol_ps, &settings);
// `getblocktemplate` - the following snapshots use a mock read_state
// get a new empty state
let read_state = MockService::build().for_unit_tests();
let make_mock_read_state_request_handler = || {
let mut read_state = read_state.clone();
async move {
read_state
.expect_request_that(|req| matches!(req, ReadRequest::ChainInfo))
.await
.respond(ReadResponse::ChainInfo(GetBlockTemplateChainInfo {
expected_difficulty: fake_difficulty,
tip_height: fake_tip_height,
tip_hash: fake_tip_hash,
cur_time: fake_cur_time,
min_time: fake_min_time,
max_time: fake_max_time,
history_tree: fake_history_tree(network),
}));
}
};
let make_mock_mempool_request_handler = || {
let mut mempool = mempool.clone();
async move {
mempool
.expect_request(mempool::Request::FullTransactions)
.await
.respond(mempool::Response::FullTransactions {
transactions: vec![],
// tip hash needs to match chain info for long poll requests
last_seen_tip_hash: fake_tip_hash,
});
}
};
// send tip hash and time needed for getblocktemplate rpc
mock_chain_tip_sender.send_best_tip_hash(fake_tip_hash);
// create a new rpc instance with new state and mock
let get_block_template_rpc_mock_state = GetBlockTemplateRpcImpl::new(
network,
mining_config.clone(),
Buffer::new(mempool.clone(), 1),
read_state.clone(),
mock_chain_tip.clone(),
block_verifier_router,
mock_sync_status.clone(),
MockAddressBookPeers::default(),
);
// Basic variant (default mode and no extra features)
// Fake the ChainInfo and FullTransaction responses
let mock_read_state_request_handler = make_mock_read_state_request_handler();
let mock_mempool_request_handler = make_mock_mempool_request_handler();
let get_block_template_fut = get_block_template_rpc_mock_state.get_block_template(None);
let (get_block_template, ..) = tokio::join!(
get_block_template_fut,
mock_mempool_request_handler,
mock_read_state_request_handler,
);
let get_block_template::Response::TemplateMode(get_block_template) =
get_block_template.expect("unexpected error in getblocktemplate RPC call")
else {
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
};
let coinbase_tx: Transaction = get_block_template
.coinbase_txn
.data
.as_ref()
.zcash_deserialize_into()
.expect("coinbase bytes are valid");
snapshot_rpc_getblocktemplate(
"basic",
(*get_block_template).into(),
Some(coinbase_tx),
&settings,
);
// long polling feature with submit old field
let long_poll_id: LongPollId = "0"
.repeat(LONG_POLL_ID_LENGTH)
.parse()
.expect("unexpected invalid LongPollId");
// Fake the ChainInfo and FullTransaction responses
let mock_read_state_request_handler = make_mock_read_state_request_handler();
let mock_mempool_request_handler = make_mock_mempool_request_handler();
let get_block_template_fut = get_block_template_rpc_mock_state.get_block_template(
get_block_template::JsonParameters {
long_poll_id: long_poll_id.into(),
..Default::default()
}
.into(),
);
let (get_block_template, ..) = tokio::join!(
get_block_template_fut,
mock_mempool_request_handler,
mock_read_state_request_handler,
);
let get_block_template::Response::TemplateMode(get_block_template) =
get_block_template.expect("unexpected error in getblocktemplate RPC call")
else {
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
};
let coinbase_tx: Transaction = get_block_template
.coinbase_txn
.data
.as_ref()
.zcash_deserialize_into()
.expect("coinbase bytes are valid");
snapshot_rpc_getblocktemplate(
"long_poll",
(*get_block_template).into(),
Some(coinbase_tx),
&settings,
);
// `getblocktemplate` proposal mode variant
let get_block_template = get_block_template_rpc_mock_state.get_block_template(Some(
get_block_template::JsonParameters {
mode: GetBlockTemplateRequestMode::Proposal,
data: Some(HexData("".into())),
..Default::default()
},
));
let get_block_template = get_block_template
.await
.expect("unexpected error in getblocktemplate RPC call");
snapshot_rpc_getblocktemplate("invalid-proposal", get_block_template, None, &settings);
// the following snapshots use a mock read_state and block_verifier_router
let mut mock_block_verifier_router = MockService::build().for_unit_tests();
let get_block_template_rpc_mock_state_verifier = GetBlockTemplateRpcImpl::new(
network,
mining_config,
Buffer::new(mempool.clone(), 1),
read_state.clone(),
mock_chain_tip,
mock_block_verifier_router.clone(),
mock_sync_status,
MockAddressBookPeers::default(),
);
let get_block_template_fut = get_block_template_rpc_mock_state_verifier.get_block_template(
Some(get_block_template::JsonParameters {
mode: GetBlockTemplateRequestMode::Proposal,
data: Some(HexData(BLOCK_MAINNET_1_BYTES.to_vec())),
..Default::default()
}),
);
let mock_block_verifier_router_request_handler = async move {
mock_block_verifier_router
.expect_request_that(|req| matches!(req, zebra_consensus::Request::CheckProposal(_)))
.await
.respond(Hash::from([0; 32]));
};
let (get_block_template, ..) = tokio::join!(
get_block_template_fut,
mock_block_verifier_router_request_handler,
);
let get_block_template =
get_block_template.expect("unexpected error in getblocktemplate RPC call");
snapshot_rpc_getblocktemplate("proposal", get_block_template, None, &settings);
// These RPC snapshots use the populated state
// `submitblock`
let submit_block = get_block_template_rpc
.submit_block(HexData("".into()), None)
.await
.expect("unexpected error in submitblock RPC call");
snapshot_rpc_submit_block_invalid(submit_block, &settings);
// `validateaddress`
let founder_address = if network.is_mainnet() {
"t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR"
} else {
"t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi"
};
let validate_address = get_block_template_rpc
.validate_address(founder_address.to_string())
.await
.expect("We should have a validate_address::Response");
snapshot_rpc_validateaddress("basic", validate_address, &settings);
let validate_address = get_block_template_rpc
.validate_address("".to_string())
.await
.expect("We should have a validate_address::Response");
snapshot_rpc_validateaddress("invalid", validate_address, &settings);
// `z_validateaddress`
let founder_address = if network.is_mainnet() {
"t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR"
} else {
"t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi"
};
let z_validate_address = get_block_template_rpc
.z_validate_address(founder_address.to_string())
.await
.expect("We should have a z_validate_address::Response");
snapshot_rpc_z_validateaddress("basic", z_validate_address, &settings);
let z_validate_address = get_block_template_rpc
.z_validate_address("".to_string())
.await
.expect("We should have a z_validate_address::Response");
snapshot_rpc_z_validateaddress("invalid", z_validate_address, &settings);
// `getdifficulty`
// This RPC snapshot uses both the mock and populated states
// Fake the ChainInfo response using the mock state
let mock_read_state_request_handler = make_mock_read_state_request_handler();
let get_difficulty_fut = get_block_template_rpc_mock_state.get_difficulty();
let (get_difficulty, ..) = tokio::join!(get_difficulty_fut, mock_read_state_request_handler,);
let mock_get_difficulty = get_difficulty.expect("unexpected error in getdifficulty RPC call");
snapshot_rpc_getdifficulty_valid("mock", mock_get_difficulty, &settings);
// Now use the populated state
let populated_get_difficulty = get_block_template_rpc.get_difficulty().await;
snapshot_rpc_getdifficulty_invalid("populated", populated_get_difficulty, &settings);
// `z_listunifiedreceivers`
let ua1 = String::from("u1l8xunezsvhq8fgzfl7404m450nwnd76zshscn6nfys7vyz2ywyh4cc5daaq0c7q2su5lqfh23sp7fkf3kt27ve5948mzpfdvckzaect2jtte308mkwlycj2u0eac077wu70vqcetkxf");
let z_list_unified_receivers =
tokio::spawn(get_block_template_rpc.z_list_unified_receivers(ua1))
.await
.expect("unexpected panic in z_list_unified_receivers RPC task")
.expect("unexpected error in z_list_unified_receivers RPC call");
snapshot_rpc_z_listunifiedreceivers("ua1", z_list_unified_receivers, &settings);
let ua2 = String::from("u1uf4qsmh037x2jp6k042h9d2w22wfp39y9cqdf8kcg0gqnkma2gf4g80nucnfeyde8ev7a6kf0029gnwqsgadvaye9740gzzpmr67nfkjjvzef7rkwqunqga4u4jges4tgptcju5ysd0");
let z_list_unified_receivers =
tokio::spawn(get_block_template_rpc.z_list_unified_receivers(ua2))
.await
.expect("unexpected panic in z_list_unified_receivers RPC task")
.expect("unexpected error in z_list_unified_receivers RPC call");
snapshot_rpc_z_listunifiedreceivers("ua2", z_list_unified_receivers, &settings);
}
/// Snapshot `getblockcount` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblockcount(block_count: u32, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_block_count", block_count));
}
/// Snapshot valid `getblockhash` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblockhash_valid(block_hash: GetBlockHash, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_block_hash_valid", block_hash));
}
/// Snapshot invalid `getblockhash` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblockhash_invalid(
variant: &'static str,
block_hash: Result<GetBlockHash>,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_block_hash_invalid_{variant}"), block_hash)
});
}
/// Snapshot `getblocktemplate` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblocktemplate(
variant: &'static str,
block_template: get_block_template::Response,
coinbase_tx: Option<Transaction>,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_block_template_{variant}"), block_template)
});
if let Some(coinbase_tx) = coinbase_tx {
settings.bind(|| {
insta::assert_ron_snapshot!(
format!("get_block_template_{variant}.coinbase_tx"),
coinbase_tx
)
});
};
}
/// Snapshot `submitblock` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_submit_block_invalid(
submit_block_response: submit_block::Response,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!("snapshot_rpc_submit_block_invalid", submit_block_response)
});
}
/// Snapshot `getmininginfo` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getmininginfo(
get_mining_info: get_mining_info::Response,
settings: &insta::Settings,
) {
settings.bind(|| insta::assert_json_snapshot!("get_mining_info", get_mining_info));
}
/// Snapshot `getblocksubsidy` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblocksubsidy(
variant: &'static str,
get_block_subsidy: BlockSubsidy,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_block_subsidy_{variant}"), get_block_subsidy)
});
}
/// Snapshot `getpeerinfo` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getpeerinfo(get_peer_info: Vec<PeerInfo>, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_peer_info", get_peer_info));
}
/// Snapshot `getnetworksolps` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getnetworksolps(get_network_sol_ps: u64, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_network_sol_ps", get_network_sol_ps));
}
/// Snapshot `validateaddress` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_validateaddress(
variant: &'static str,
validate_address: validate_address::Response,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("validate_address_{variant}"), validate_address)
});
}
/// Snapshot `z_validateaddress` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_z_validateaddress(
variant: &'static str,
z_validate_address: z_validate_address::Response,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("z_validate_address_{variant}"), z_validate_address)
});
}
/// Snapshot valid `getdifficulty` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getdifficulty_valid(
variant: &'static str,
difficulty: f64,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_difficulty_valid_{variant}"), difficulty)
});
}
/// Snapshot invalid `getdifficulty` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getdifficulty_invalid(
variant: &'static str,
difficulty: Result<f64>,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("get_difficulty_invalid_{variant}"), difficulty)
});
}
/// Snapshot `snapshot_rpc_z_listunifiedreceivers` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_z_listunifiedreceivers(
variant: &'static str,
response: unified_address::Response,
settings: &insta::Settings,
) {
settings.bind(|| {
insta::assert_json_snapshot!(format!("z_list_unified_receivers_{variant}"), response)
});
}