diff --git a/.github/workflows/tests-p2p.yml b/.github/workflows/tests-p2p.yml index c1a7ad9373..cbdbe46958 100644 --- a/.github/workflows/tests-p2p.yml +++ b/.github/workflows/tests-p2p.yml @@ -54,7 +54,6 @@ jobs: - net::tests::convergence::test_walk_star_15_pingback - net::tests::convergence::test_walk_star_15_org_biased - net::tests::convergence::test_walk_inbound_line_15 - - net::api::tests::postblock_proposal::test_try_make_response - net::server::test::test_http_10_threads_getinfo - net::server::test::test_http_10_threads_getblock - net::server::test::test_http_too_many_clients diff --git a/stackslib/src/net/api/tests/getmapentry.rs b/stackslib/src/net/api/tests/getmapentry.rs index 85b45638cc..326e260d13 100644 --- a/stackslib/src/net/api/tests/getmapentry.rs +++ b/stackslib/src/net/api/tests/getmapentry.rs @@ -17,7 +17,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use clarity::types::chainstate::StacksBlockId; -use clarity::vm::types::QualifiedContractIdentifier; +use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions, TupleData}; use clarity::vm::{ClarityName, Value}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::types::Address; @@ -146,6 +146,27 @@ fn test_try_make_response() { ); requests.push(request); + // query existing with a tuple-typed key and without a MARF proof + let account = StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") + .unwrap() + .to_account_principal(); + let request = StacksHttpRequest::new_getmapentry( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + "unit-map".try_into().unwrap(), + Value::Tuple( + TupleData::from_data(vec![( + ClarityName::from_literal("account"), + Value::Principal(account), + )]) + .unwrap(), + ), + TipRequest::UseLatestAnchoredTip, + false, + ); + requests.push(request); + let mut responses = test_rpc(function_name!(), requests); // latest data @@ -191,196 +212,20 @@ fn test_try_make_response() { let resp = response.decode_map_entry_response().unwrap(); assert_eq!(resp.data, "0x09"); assert_eq!(resp.marf_proof, Some("".to_string())); -} -/* -#[test] -#[ignore] -fn test_rpc_get_map_entry() { - // Test v2/map_entry (aka GetMapEntry) endpoint. - // In this test, we don't set any tip parameters, and we expect that querying for map data - // against the canonical Stacks tip will succeed. - test_rpc( - function_name!(), - 40130, - 40131, - 50130, - 50131, - true, - |ref mut peer_client, - ref mut convo_client, - ref mut peer_server, - ref mut convo_server| { - let principal = - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap() - .to_account_principal(); - convo_client.new_getmapentry( - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap(), - "hello-world".try_into().unwrap(), - "unit-map".try_into().unwrap(), - Value::Tuple( - TupleData::from_data(vec![("account".into(), Value::Principal(principal))]) - .unwrap(), - ), - TipRequest::UseLatestAnchoredTip, - false, - ) - }, - |ref http_request, - ref http_response, - ref mut peer_client, - ref mut peer_server, - ref convo_client, - ref convo_server| { - let req_md = http_request.preamble().clone(); - match http_response { - HttpResponseType::GetMapEntry(response_md, data) => { - assert_eq!( - Value::try_deserialize_hex_untyped(&data.data).unwrap(), - Value::some(Value::Tuple( - TupleData::from_data(vec![("units".into(), Value::Int(123))]) - .unwrap() - )) - .unwrap() - ); - true - } - _ => { - error!("Invalid response; {:?}", &http_response); - false - } - } - }, - ); -} - -#[test] -#[ignore] -fn test_rpc_get_map_entry_unconfirmed() { - // Test v2/map_entry (aka GetMapEntry) endpoint. - // In this test, we set `tip_req` to UseLatestUnconfirmedTip, and we expect that querying for map data - // against the unconfirmed state will succeed. - test_rpc( - function_name!(), - 40140, - 40141, - 50140, - 50141, - true, - |ref mut peer_client, - ref mut convo_client, - ref mut peer_server, - ref mut convo_server| { - let unconfirmed_tip = peer_client - .chainstate() - .unconfirmed_state - .as_ref() - .unwrap() - .unconfirmed_chain_tip - .clone(); - let principal = - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap() - .to_account_principal(); - convo_client.new_getmapentry( - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap(), - "hello-world".try_into().unwrap(), - "unit-map".try_into().unwrap(), - Value::Tuple( - TupleData::from_data(vec![("account".into(), Value::Principal(principal))]) - .unwrap(), - ), - TipRequest::SpecificTip(unconfirmed_tip), - false, - ) - }, - |ref http_request, - ref http_response, - ref mut peer_client, - ref mut peer_server, - ref convo_client, - ref convo_server| { - let req_md = http_request.preamble().clone(); - match http_response { - HttpResponseType::GetMapEntry(response_md, data) => { - assert_eq!( - Value::try_deserialize_hex_untyped(&data.data).unwrap(), - Value::some(Value::Tuple( - TupleData::from_data(vec![("units".into(), Value::Int(1))]) - .unwrap() - )) - .unwrap() - ); - true - } - _ => { - error!("Invalid response; {:?}", &http_response); - false - } - } - }, + // tuple key, no proof + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() ); -} -#[test] -#[ignore] -fn test_rpc_get_map_entry_use_latest_tip() { - test_rpc( - function_name!(), - 40142, - 40143, - 50142, - 50143, - true, - |ref mut peer_client, - ref mut convo_client, - ref mut peer_server, - ref mut convo_server| { - let principal = - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap() - .to_account_principal(); - convo_client.new_getmapentry( - StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") - .unwrap(), - "hello-world".try_into().unwrap(), - "unit-map".try_into().unwrap(), - Value::Tuple( - TupleData::from_data(vec![("account".into(), Value::Principal(principal))]) - .unwrap(), - ), - TipRequest::UseLatestAnchoredTip, - false, - ) - }, - |ref http_request, - ref http_response, - ref mut peer_client, - ref mut peer_server, - ref convo_client, - ref convo_server| { - let req_md = http_request.preamble().clone(); - match http_response { - HttpResponseType::GetMapEntry(response_md, data) => { - assert_eq!( - Value::try_deserialize_hex_untyped(&data.data).unwrap(), - Value::some(Value::Tuple( - TupleData::from_data(vec![("units".into(), Value::Int(1))]) - .unwrap() - )) - .unwrap() - ); - true - } - _ => { - error!("Invalid response; {:?}", &http_response); - false - } - } - }, + let resp = response.decode_map_entry_response().unwrap(); + // `(some (tuple (units 123)))`, the value set for this principal at deploy time + assert_eq!( + resp.data, + "0x0a0c0000000105756e697473000000000000000000000000000000007b" ); + // `with_proof = false`, so no MARF proof is returned + assert_eq!(resp.marf_proof, None); } -*/ diff --git a/stackslib/src/net/api/tests/postblock_proposal.rs b/stackslib/src/net/api/tests/postblock_proposal.rs index 152bde08f7..ae0f05333b 100644 --- a/stackslib/src/net/api/tests/postblock_proposal.rs +++ b/stackslib/src/net/api/tests/postblock_proposal.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// Copyright (C) 2020-2026 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -57,7 +57,6 @@ use crate::net::relay::Relayer; use crate::net::test::{TestEventObserver, TestPeer}; use crate::net::ProtocolFamily; -#[warn(unused)] #[test] fn test_try_parse_request() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); @@ -230,7 +229,6 @@ impl MemPoolEventDispatcher for ProposalTestObserver { } #[test] -#[ignore] fn test_try_make_response() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); let test_observer = TestEventObserver::new(); @@ -514,12 +512,11 @@ fn test_try_make_response() { } } -/// Test that when block validation exceeds the deadline, the rejection -/// includes the txid of the transaction at which the deadline was exceeded -/// so the miner can exclude it from the next block proposal. +/// Test that when block validation exceeds the overall block-level deadline +/// (`block_proposal_validation_timeout_secs`), the block is rejected as an +/// `InvalidBlock` without blaming any specific transaction. #[test] -#[ignore] -fn test_block_proposal_validation_timeout_blames_tx() { +fn test_block_proposal_validation_timeout() { let test_observer = TestEventObserver::new(); let mut rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer); @@ -694,14 +691,194 @@ fn test_block_proposal_validation_timeout_blames_tx() { failed_txid, .. }) => { - assert_eq!(reason_code, ValidateRejectCode::BadTransaction); + assert_eq!(reason_code, ValidateRejectCode::InvalidBlock); + assert!( + failed_txid.is_none(), + "Block-level timeout is the block's fault and must not blame any tx" + ); assert!( - failed_txid.is_some(), - "Timeout rejection should blame the tx at which the deadline was exceeded" + reason.contains("timed out"), + "Expected rejection reason to mention the timeout, got: {reason}" + ); + } + } +} + +/// Test that when a transaction's execution phase exceeds the dedicated per-tx +/// execution budget (`block_proposal_max_tx_execution_time_secs`) during +/// block-proposal validation, the block is rejected as containing a problematic +/// transaction, and the rejection blames the offending txid so the miner can +/// drop it from the next proposal. +#[test] +fn test_block_proposal_validation_execution_time_expired_blames_tx() { + let test_observer = TestEventObserver::new(); + let mut rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer); + + // Force every tx execution to exceed its budget: a 0s deadline is already + // elapsed at the first per-node check. The overall validation timeout and + // the per-tx analysis limit are left at their (non-zero) defaults, so the + // per-tx execution limit is what fires here, not the block-level deadline + // nor the analysis budget. + rpc_test + .peer_1 + .network + .connection_opts + .block_proposal_max_tx_execution_time_secs = 0; + rpc_test + .peer_2 + .network + .connection_opts + .block_proposal_max_tx_execution_time_secs = 0; + + let (stacks_tip_ch, stacks_tip_bhh) = SortitionDB::get_canonical_stacks_chain_tip_hash( + rpc_test.peer_1.chain.sortdb.as_ref().unwrap().conn(), + ) + .unwrap(); + let stacks_tip = StacksBlockId::new(&stacks_tip_ch, &stacks_tip_bhh); + + let miner_privk = &rpc_test.peer_1.chain.miner.nakamoto_miner_key(); + // A top-level initializer (`define-data-var`) forces work during the contract's + // initialization phase. + let contract_code = "(define-data-var counter uint (+ u1 u1)) (define-public (ping) (ok u0))"; + + let deploy_tx_bytes = make_contract_publish( + miner_privk, + 36, + 1000, + CHAIN_ID_TESTNET, + "execution-time-contract", + contract_code, + ); + let deploy_tx = + StacksTransaction::consensus_deserialize(&mut deploy_tx_bytes.as_slice()).unwrap(); + + // Build a valid block containing the deploy. This builder has no execution + // limit, so the contract publishes cleanly; the execution budget is applied + // only later, by proposal validation. + let mut block = { + let chainstate = rpc_test.peer_1.chainstate(); + let parent_stacks_header = + NakamotoChainState::get_block_header(chainstate.db(), &stacks_tip) + .unwrap() + .unwrap(); + + let mut builder = NakamotoBlockBuilder::new( + &parent_stacks_header, + &parent_stacks_header.consensus_hash, + 26000, + None, + None, + 8, + None, + None, + None, + u64::from(DEFAULT_MAX_TENURE_BYTES), + ) + .unwrap(); + + rpc_test + .peer_1 + .with_db_state( + |sort_db: &mut SortitionDB, + chainstate: &mut StacksChainState, + _: &mut Relayer, + _: &mut MemPoolDB| { + let burn_dbconn = sort_db.index_handle_at_tip(); + let mut miner_tenure_info = builder + .load_tenure_info( + chainstate, + &burn_dbconn, + MinerTenureInfoCause::NoTenureChange, + ) + .unwrap(); + let burn_chain_height = miner_tenure_info.burn_tip_height; + let mut tenure_tx = builder + .tenure_begin(&burn_dbconn, &mut miner_tenure_info) + .unwrap(); + let tx_result = builder.try_mine_tx_with_len( + &mut tenure_tx, + &deploy_tx, + deploy_tx.tx_len(), + &BlockLimitFunction::NO_LIMIT_HIT, + None, + &mut 0, + ); + assert!(matches!(tx_result, TransactionResult::Success(_))); + let block = builder.mine_nakamoto_block(&mut tenure_tx, burn_chain_height); + Ok(block) + }, + ) + .unwrap() + }; + + block.header.timestamp += 1; + rpc_test.peer_1.chain.miner.sign_nakamoto_block(&mut block); + + let proposal = NakamotoBlockProposal { + block, + chain_id: CHAIN_ID_TESTNET, + replay_txs: None, + }; + + let mut request = StacksHttpRequest::new_for_peer( + rpc_test.peer_1.to_peer_host(), + "POST".into(), + "/v3/block_proposal".into(), + HttpRequestContents::new().payload_json(serde_json::to_value(proposal).unwrap()), + ) + .expect("failed to construct request"); + request.add_header("authorization".into(), "password".into()); + + let observer = ProposalTestObserver::new(); + let proposal_observer = Arc::clone(&observer.proposal_observer); + + let wait_for = |peer_1: &mut TestPeer, peer_2: &mut TestPeer| { + !peer_1.network.is_proposal_thread_running() && !peer_2.network.is_proposal_thread_running() + }; + + let responses = rpc_test.run_with_observer(vec![request], Some(&observer), wait_for); + + assert_eq!(responses.len(), 1); + assert_eq!(responses[0].preamble().status_code, 202); + + let start = Instant::now(); + loop { + { + let observer_guard = proposal_observer.lock().unwrap(); + if observer_guard.results.lock().unwrap().len() >= 1 { + break; + } + } + assert!( + start.elapsed().as_secs() < 60, + "Timed out waiting for proposal result" + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + let observer_guard = proposal_observer.lock().unwrap(); + let mut results = observer_guard.results.lock().unwrap(); + let result = results.remove(0); + drop(results); + drop(observer_guard); + + match result { + Ok(_) => panic!("expected execution-time-expired tx to reject block"), + Err(postblock_proposal::BlockValidateReject { + reason_code, + reason, + failed_txid, + .. + }) => { + assert_eq!(reason_code, ValidateRejectCode::ProblematicTransaction); + assert_eq!( + failed_txid, + Some(deploy_tx.txid()), + "Rejection should blame the tx whose execution timed out" ); assert!( - reason.contains("exceeded deadline"), - "Expected rejection reason to mention deadline, got: {reason}" + reason.contains("execution time expired"), + "Expected rejection reason to mention execution time, got: {reason}" ); } } @@ -1029,7 +1206,6 @@ fn replay_validation_test( } #[test] -#[ignore] /// Tx replay test with mismatching mineable transactions. fn replay_validation_test_transaction_mismatch() { let result = replay_validation_test(|rpc_test| { @@ -1069,7 +1245,6 @@ fn replay_validation_test_transaction_mismatch() { } #[test] -#[ignore] /// Replay set has one unmineable tx, and one mineable tx. /// The block has the one mineable tx. fn replay_validation_test_transaction_unmineable_match() { @@ -1109,7 +1284,6 @@ fn replay_validation_test_transaction_unmineable_match() { } #[test] -#[ignore] /// Replay set has [mineable, unmineable, mineable] /// The block has [mineable, mineable] fn replay_validation_test_transaction_unmineable_match_2() { @@ -1165,7 +1339,6 @@ fn replay_validation_test_transaction_unmineable_match_2() { } #[test] -#[ignore] /// Replay set has [mineable, mineable, tx_a, mineable] /// The block has [mineable, mineable, tx_b, mineable] fn replay_validation_test_transaction_mineable_mismatch_series() { @@ -1243,7 +1416,6 @@ fn replay_validation_test_transaction_mineable_mismatch_series() { } #[test] -#[ignore] /// Replay set has [mineable, tx_b, tx_a] /// The block has [mineable, tx_a, tx_b] fn replay_validation_test_transaction_mineable_mismatch_series_2() { @@ -1301,7 +1473,6 @@ fn replay_validation_test_transaction_mineable_mismatch_series_2() { } #[test] -#[ignore] /// Replay set has [deploy, big_a, big_b, c] /// The block has [deploy, big_a, c] /// @@ -1379,7 +1550,6 @@ fn replay_validation_test_budget_exceeded() { } #[test] -#[ignore] /// Replay set has [deploy, big_a, big_b] /// The block has [deploy, big_a] ///