//! 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( 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, >::Future: Send, ReadState: Service< zebra_state::ReadRequest, Response = zebra_state::ReadResponse, Error = zebra_state::BoxError, > + Clone + Send + Sync + 'static, >::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, 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, 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, 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, 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) }); }