diff --git a/Cargo.lock b/Cargo.lock index ac254bb04c..483023e59a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,6 +504,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "c-enum" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd17eb909a8c6a894926bfcc3400a4bb0e732f5a57d37b1f14e8b29e329bace8" + [[package]] name = "cc" version = "1.0.83" @@ -1990,6 +1996,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.5" @@ -1999,6 +2014,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2135,7 +2159,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2257,6 +2281,41 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "perf-event-data" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575828d9d7d205188048eb1508560607a03d21eafdbba47b8cade1736c1c28e1" +dependencies = [ + "bitflags 2.4.2", + "c-enum", + "perf-event-open-sys2", +] + +[[package]] +name = "perf-event-open-sys2" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c25955321465255e437600b54296983fab1feac2cd0c38958adeb26dbae49e" +dependencies = [ + "libc", + "memoffset 0.9.1", +] + +[[package]] +name = "perf-event2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0939b8fad77dfaeb29ebbd35faaeaadbf833167f30975f1b8993bbba09ea0a0f" +dependencies = [ + "bitflags 2.4.2", + "c-enum", + "libc", + "memmap2", + "perf-event-data", + "perf-event-open-sys2", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -3532,6 +3591,7 @@ dependencies = [ "mutants", "nix", "percent-encoding", + "perf-event2", "pox-locking 2.4.0", "prometheus", "proptest", diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index 0b8d920ab3..747aaf16b4 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -81,6 +81,10 @@ developer-mode = ["clarity/developer-mode"] monitoring_prom = ["prometheus"] slog_json = ["stacks-common/slog_json", "clarity/slog_json", "pox-locking/slog_json"] testing = ["chrono", "stacks-common/testing", "clarity/testing"] +profiler = ["perf-event2"] + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] +perf-event2 = { version = "0.7.4", optional = true } [target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(any(target_os="windows"))))'.dependencies] sha2 = { version = "0.10", features = ["asm"] } diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index dae996501c..f148ff4405 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -606,6 +606,10 @@ impl<'a, 'b> ClarityTx<'a, 'b> { }) .expect("FATAL: `ust-liquid-supply` overflowed"); } + + pub fn disable_fees(&mut self) { + self.block.no_fees = true; + } } pub struct ChainstateTx<'a> { diff --git a/stackslib/src/chainstate/stacks/db/transactions.rs b/stackslib/src/chainstate/stacks/db/transactions.rs index 46dd54b041..11c9ff40f9 100644 --- a/stackslib/src/chainstate/stacks/db/transactions.rs +++ b/stackslib/src/chainstate/stacks/db/transactions.rs @@ -564,6 +564,7 @@ impl StacksChainState { } StacksChainState::account_debit(clarity_tx, &payer_account.principal, fee); + Ok(fee) } @@ -1554,6 +1555,8 @@ impl StacksChainState { debug!("Process transaction {} ({})", tx.txid(), tx.payload.name()); let epoch = clarity_block.get_epoch(); + let no_fees = clarity_block.block.no_fees; + StacksChainState::process_transaction_precheck(&clarity_block.config, tx, epoch)?; // what version of Clarity did the transaction caller want? And, is it valid now? @@ -1578,7 +1581,10 @@ impl StacksChainState { let payer_address = payer_account.principal.clone(); let payer_nonce = payer_account.nonce; - StacksChainState::pay_transaction_fee(&mut transaction, fee, payer_account)?; + + if !no_fees { + StacksChainState::pay_transaction_fee(&mut transaction, fee, payer_account)?; + } // origin balance may have changed (e.g. if the origin paid the tx fee), so reload the account let origin_account = @@ -1619,8 +1625,10 @@ impl StacksChainState { None, )?; - let new_payer_account = StacksChainState::get_payer_account(&mut transaction, tx); - StacksChainState::pay_transaction_fee(&mut transaction, fee, new_payer_account)?; + if !no_fees { + let new_payer_account = StacksChainState::get_payer_account(&mut transaction, tx); + StacksChainState::pay_transaction_fee(&mut transaction, fee, new_payer_account)?; + } // update the account nonces StacksChainState::update_account_nonce( diff --git a/stackslib/src/clarity_vm/clarity.rs b/stackslib/src/clarity_vm/clarity.rs index b563bbbc95..9855d9f0aa 100644 --- a/stackslib/src/clarity_vm/clarity.rs +++ b/stackslib/src/clarity_vm/clarity.rs @@ -117,6 +117,7 @@ pub struct ClarityBlockConnection<'a, 'b> { mainnet: bool, chain_id: u32, epoch: StacksEpochId, + pub no_fees: bool, } /// @@ -318,6 +319,7 @@ impl ClarityBlockConnection<'_, '_> { mainnet: false, chain_id: CHAIN_ID_TESTNET, epoch, + no_fees: false, } } @@ -444,6 +446,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch: epoch.epoch_id, + no_fees: false, } } @@ -468,6 +471,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch, + no_fees: false, } } @@ -494,6 +498,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch, + no_fees: false, }; let use_mainnet = self.mainnet; @@ -590,6 +595,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch, + no_fees: false, }; let use_mainnet = self.mainnet; @@ -698,6 +704,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch: epoch.epoch_id, + no_fees: false, } } @@ -738,6 +745,7 @@ impl ClarityInstance { mainnet: self.mainnet, chain_id: self.chain_id, epoch: epoch.epoch_id, + no_fees: false, } } diff --git a/stackslib/src/net/api/blockreplay.rs b/stackslib/src/net/api/blockreplay.rs index f9b48db20e..c8a7080792 100644 --- a/stackslib/src/net/api/blockreplay.rs +++ b/stackslib/src/net/api/blockreplay.rs @@ -22,6 +22,7 @@ use stacks_common::types::net::PeerHost; use stacks_common::util::hash::Sha512Trunc256Sum; use stacks_common::util::secp256k1::MessageSignature; use stacks_common::util::serde_serializers::prefix_hex_codec; +use url::form_urlencoded; use crate::burnchains::Txid; use crate::chainstate::burn::db::sortdb::SortitionDB; @@ -39,10 +40,114 @@ use crate::net::http::{ use crate::net::httpcore::{RPCRequestHandler, StacksHttpResponse}; use crate::net::{Error as NetError, StacksHttpRequest, StacksNodeState}; +#[cfg(all(feature = "profiler", target_os = "linux", target_arch = "x86_64"))] +struct BlockReplayProfiler { + perf_event_cpu_instructions: Option, + perf_event_cpu_cycles: Option, + perf_event_cpu_ref_cycles: Option, +} + +#[cfg(not(all(feature = "profiler", target_os = "linux", target_arch = "x86_64")))] +struct BlockReplayProfiler(); + +#[derive(Default)] +pub struct BlockReplayProfilerResult { + cpu_instructions: Option, + cpu_cycles: Option, + cpu_ref_cycles: Option, +} + +#[cfg(all(feature = "profiler", target_os = "linux", target_arch = "x86_64"))] +impl BlockReplayProfiler { + fn new() -> Self { + let mut perf_event_cpu_instructions: Option = None; + let mut perf_event_cpu_cycles: Option = None; + let mut perf_event_cpu_ref_cycles: Option = None; + + if let Ok(mut perf_event_cpu_instructions_result) = + perf_event::Builder::new(perf_event::events::Hardware::INSTRUCTIONS).build() + { + if perf_event_cpu_instructions_result.enable().is_ok() { + perf_event_cpu_instructions = Some(perf_event_cpu_instructions_result); + } + } + + if let Ok(mut perf_event_cpu_cycles_result) = + perf_event::Builder::new(perf_event::events::Hardware::CPU_CYCLES).build() + { + if perf_event_cpu_cycles_result.enable().is_ok() { + perf_event_cpu_cycles = Some(perf_event_cpu_cycles_result); + } + } + + if let Ok(mut perf_event_cpu_ref_cycles_result) = + perf_event::Builder::new(perf_event::events::Hardware::REF_CPU_CYCLES).build() + { + if perf_event_cpu_ref_cycles_result.enable().is_ok() { + perf_event_cpu_ref_cycles = Some(perf_event_cpu_ref_cycles_result); + } + } + + Self { + perf_event_cpu_instructions, + perf_event_cpu_cycles, + perf_event_cpu_ref_cycles, + } + } + + fn collect(self) -> BlockReplayProfilerResult { + let mut cpu_instructions: Option = None; + let mut cpu_cycles: Option = None; + let mut cpu_ref_cycles: Option = None; + + if let Some(mut perf_event_cpu_instructions) = self.perf_event_cpu_instructions { + if perf_event_cpu_instructions.disable().is_ok() { + if let Ok(value) = perf_event_cpu_instructions.read() { + cpu_instructions = Some(value); + } + } + } + + if let Some(mut perf_event_cpu_cycles) = self.perf_event_cpu_cycles { + if perf_event_cpu_cycles.disable().is_ok() { + if let Ok(value) = perf_event_cpu_cycles.read() { + cpu_cycles = Some(value); + } + } + } + + if let Some(mut perf_event_cpu_ref_cycles) = self.perf_event_cpu_ref_cycles { + if perf_event_cpu_ref_cycles.disable().is_ok() { + if let Ok(value) = perf_event_cpu_ref_cycles.read() { + cpu_ref_cycles = Some(value); + } + } + } + + BlockReplayProfilerResult { + cpu_instructions, + cpu_cycles, + cpu_ref_cycles, + } + } +} + +#[cfg(not(all(feature = "profiler", target_os = "linux", target_arch = "x86_64")))] +impl BlockReplayProfiler { + fn new() -> Self { + warn!("BlockReplay Profiler is not available in this build."); + Self {} + } + fn collect(self) -> BlockReplayProfilerResult { + BlockReplayProfilerResult::default() + } +} + #[derive(Clone)] pub struct RPCNakamotoBlockReplayRequestHandler { pub block_id: Option, pub auth: Option, + pub profiler: bool, } impl RPCNakamotoBlockReplayRequestHandler { @@ -50,6 +155,7 @@ impl RPCNakamotoBlockReplayRequestHandler { Self { block_id: None, auth, + profiler: false, } } @@ -160,6 +266,13 @@ impl RPCNakamotoBlockReplayRequestHandler { for (i, tx) in block.txs.iter().enumerate() { let tx_len = tx.tx_len(); + let mut profiler: Option = None; + let mut profiler_result = BlockReplayProfilerResult::default(); + + if self.profiler { + profiler = Some(BlockReplayProfiler::new()); + } + let tx_result = builder.try_mine_tx_with_len( &mut tenure_tx, tx, @@ -167,9 +280,14 @@ impl RPCNakamotoBlockReplayRequestHandler { &BlockLimitFunction::NO_LIMIT_HIT, None, ); + + if let Some(profiler) = profiler { + profiler_result = profiler.collect(); + } + let err = match tx_result { TransactionResult::Success(tx_result) => { - txs_receipts.push(tx_result.receipt); + txs_receipts.push((tx_result.receipt, profiler_result)); Ok(()) } _ => Err(format!("Problematic tx {i}")), @@ -192,10 +310,10 @@ impl RPCNakamotoBlockReplayRequestHandler { let tx_merkle_root = block.header.tx_merkle_root.clone(); let mut rpc_replayed_block = - RPCReplayedBlock::from_block(block, block_fees, tenure_id, parent_block_id); + RPCReplayedBlock::from_block(&replayed_block, block_fees, tenure_id, parent_block_id); - for receipt in &txs_receipts { - let transaction = RPCReplayedBlockTransaction::from_receipt(receipt); + for (receipt, profiler_result) in &txs_receipts { + let transaction = RPCReplayedBlockTransaction::from_receipt(receipt, &profiler_result); rpc_replayed_block.transactions.push(transaction); } @@ -231,10 +349,17 @@ pub struct RPCReplayedBlockTransaction { pub post_condition_aborted: bool, /// optional vm error pub vm_error: Option, + /// profiling data based on linux perf_events + pub cpu_instructions: Option, + pub cpu_cycles: Option, + pub cpu_ref_cycles: Option, } impl RPCReplayedBlockTransaction { - pub fn from_receipt(receipt: &StacksTransactionReceipt) -> Self { + pub fn from_receipt( + receipt: &StacksTransactionReceipt, + profiler_result: &BlockReplayProfilerResult, + ) -> Self { let events = receipt .events .iter() @@ -269,6 +394,9 @@ impl RPCReplayedBlockTransaction { events, post_condition_aborted: receipt.post_condition_aborted, vm_error: receipt.vm_error.clone(), + cpu_instructions: profiler_result.cpu_instructions, + cpu_cycles: profiler_result.cpu_cycles, + cpu_ref_cycles: profiler_result.cpu_ref_cycles, } } } @@ -305,7 +433,7 @@ pub struct RPCReplayedBlock { impl RPCReplayedBlock { pub fn from_block( - block: NakamotoBlock, + block: &NakamotoBlock, block_fees: u128, tenure_id: ConsensusHash, parent_block_id: StacksBlockId, @@ -320,11 +448,11 @@ impl RPCReplayedBlock { parent_block_id, consensus_hash: tenure_id, fees: block_fees, - tx_merkle_root: block.header.tx_merkle_root, + tx_merkle_root: block.header.tx_merkle_root.clone(), state_index_root: block.header.state_index_root, timestamp: block.header.timestamp, - miner_signature: block.header.miner_signature, - signer_signature: block.header.signer_signature, + miner_signature: block.header.miner_signature.clone(), + signer_signature: block.header.signer_signature.clone(), transactions: vec![], valid_merkle_root: false, } @@ -382,6 +510,17 @@ impl HttpRequest for RPCNakamotoBlockReplayRequestHandler { self.block_id = Some(block_id); + if let Some(query_string) = query { + for (key, value) in form_urlencoded::parse(query_string.as_bytes()) { + if key == "profiler" { + if value == "1" { + self.profiler = true; + } + break; + } + } + } + Ok(HttpRequestContents::new().query_string(query)) } } @@ -446,6 +585,23 @@ impl StacksHttpRequest { ) .expect("FATAL: failed to construct request from infallible data") } + + pub fn new_block_replay_with_profiler( + host: PeerHost, + block_id: &StacksBlockId, + profiler: bool, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v3/blocks/replay/{block_id}"), + HttpRequestContents::new().query_arg( + "profiler".into(), + if profiler { "1".into() } else { "0".into() }, + ), + ) + .expect("FATAL: failed to construct request from infallible data") + } } /// Decode the HTTP response diff --git a/stackslib/src/net/api/blocksimulate.rs b/stackslib/src/net/api/blocksimulate.rs new file mode 100644 index 0000000000..ff59d2f255 --- /dev/null +++ b/stackslib/src/net/api/blocksimulate.rs @@ -0,0 +1,435 @@ +// Copyright (C) 2025 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 +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity::vm::costs::ExecutionCost; +use clarity::vm::Value; +use regex::{Captures, Regex}; +use stacks_common::codec::{Error as CodecError, StacksMessageCodec, MAX_PAYLOAD_LEN}; +use stacks_common::types::chainstate::{BlockHeaderHash, ConsensusHash, StacksBlockId, TrieHash}; +use stacks_common::types::net::PeerHost; +use stacks_common::util::hash::{hex_bytes, Sha512Trunc256Sum}; +use stacks_common::util::secp256k1::MessageSignature; +use stacks_common::util::serde_serializers::prefix_hex_codec; +use url::form_urlencoded; + +use crate::burnchains::Txid; +use crate::chainstate::burn::db::sortdb::SortitionDB; +use crate::chainstate::nakamoto::miner::{MinerTenureInfoCause, NakamotoBlockBuilder}; +use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; +use crate::chainstate::stacks::db::StacksChainState; +use crate::chainstate::stacks::events::{StacksTransactionReceipt, TransactionOrigin}; +use crate::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction, TransactionResult}; +use crate::chainstate::stacks::{Error as ChainError, StacksTransaction, TransactionPayload}; +use crate::config::DEFAULT_MAX_TENURE_BYTES; +use crate::net::api::blockreplay::{ + BlockReplayProfilerResult, RPCReplayedBlock, RPCReplayedBlockTransaction, +}; +use crate::net::http::{ + parse_json, Error, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents, + HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload, + HttpResponsePreamble, HttpServerError, +}; +use crate::net::httpcore::{RPCRequestHandler, StacksHttpResponse}; +use crate::net::{Error as NetError, StacksHttpRequest, StacksNodeState}; + +#[derive(Clone)] +pub struct RPCNakamotoBlockSimulateRequestHandler { + pub block_id: Option, + pub auth: Option, + pub profiler: bool, + pub transactions: Vec, +} + +impl RPCNakamotoBlockSimulateRequestHandler { + pub fn new(auth: Option) -> Self { + Self { + block_id: None, + auth, + profiler: false, + transactions: vec![], + } + } + + fn parse_json(body: &[u8]) -> Result, Error> { + let transactions_hex: Vec = serde_json::from_slice(body) + .map_err(|e| Error::DecodeError(format!("Failed to parse body: {e}")))?; + + let mut transactions = vec![]; + + for tx_hex in transactions_hex { + let tx_bytes = + hex_bytes(&tx_hex).map_err(|_e| Error::DecodeError("Failed to parse tx".into()))?; + let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).map_err(|e| { + if let CodecError::DeserializeError(msg) = e { + Error::DecodeError(format!("Failed to deserialize transaction: {}", msg)) + } else { + e.into() + } + })?; + transactions.push(tx); + } + + Ok(transactions) + } + + pub fn block_simulate( + &self, + sortdb: &SortitionDB, + chainstate: &mut StacksChainState, + ) -> Result { + let Some(block_id) = &self.block_id else { + return Err(ChainError::InvalidStacksBlock("block_id is None".into())); + }; + + let Some((tenure_id, parent_block_id)) = chainstate + .nakamoto_blocks_db() + .get_tenure_and_parent_block_id(&block_id)? + else { + return Err(ChainError::NoSuchBlockError); + }; + + let staging_db_path = chainstate.get_nakamoto_staging_blocks_path()?; + let db_conn = StacksChainState::open_nakamoto_staging_blocks(&staging_db_path, false)?; + let rowid = db_conn + .conn() + .get_nakamoto_block_rowid(&block_id)? + .ok_or(ChainError::NoSuchBlockError)?; + + let mut blob_fd = match db_conn.open_nakamoto_block(rowid, false).map_err(|e| { + let msg = format!("Failed to open Nakamoto block {}: {:?}", &block_id, &e); + warn!("{}", &msg); + msg + }) { + Ok(blob_fd) => blob_fd, + Err(e) => return Err(ChainError::InvalidStacksBlock(e)), + }; + + let block = match NakamotoBlock::consensus_deserialize(&mut blob_fd).map_err(|e| { + let msg = format!("Failed to read Nakamoto block {}: {:?}", &block_id, &e); + warn!("{}", &msg); + msg + }) { + Ok(block) => block, + Err(e) => return Err(ChainError::InvalidStacksBlock(e)), + }; + + let burn_dbconn = match sortdb.index_handle_at_block(chainstate, &parent_block_id) { + Ok(burn_dbconn) => burn_dbconn, + Err(_) => return Err(ChainError::NoSuchBlockError), + }; + + let tenure_change = block + .txs + .iter() + .find(|tx| matches!(tx.payload, TransactionPayload::TenureChange(..))); + let coinbase = block + .txs + .iter() + .find(|tx| matches!(tx.payload, TransactionPayload::Coinbase(..))); + let tenure_cause = tenure_change + .and_then(|tx| match &tx.payload { + TransactionPayload::TenureChange(tc) => Some(tc.into()), + _ => None, + }) + .unwrap_or(MinerTenureInfoCause::NoTenureChange); + + let parent_stacks_header_opt = + match NakamotoChainState::get_block_header(chainstate.db(), &parent_block_id) { + Ok(parent_stacks_header_opt) => parent_stacks_header_opt, + Err(e) => return Err(e), + }; + + let Some(parent_stacks_header) = parent_stacks_header_opt else { + return Err(ChainError::InvalidStacksBlock( + "Invalid Parent Block".into(), + )); + }; + + let mut builder = match NakamotoBlockBuilder::new( + &parent_stacks_header, + &block.header.consensus_hash, + block.header.burn_spent, + tenure_change, + coinbase, + block.header.pox_treatment.len(), + None, + None, + Some(block.header.timestamp), + u64::from(DEFAULT_MAX_TENURE_BYTES), + ) { + Ok(builder) => builder, + Err(e) => return Err(e), + }; + + let mut miner_tenure_info = + match builder.load_ephemeral_tenure_info(chainstate, &burn_dbconn, tenure_cause) { + Ok(miner_tenure_info) => miner_tenure_info, + Err(e) => return Err(e), + }; + + let burn_chain_height = miner_tenure_info.burn_tip_height; + let mut tenure_tx = match builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info) { + Ok(tenure_tx) => tenure_tx, + Err(e) => return Err(e), + }; + + tenure_tx.disable_fees(); + + let mut block_fees: u128 = 0; + let mut txs_receipts = vec![]; + + for (i, tx) in self.transactions.iter().enumerate() { + let tx_len = tx.tx_len(); + + let tx_result = builder.try_mine_tx_with_len( + &mut tenure_tx, + tx, + tx_len, + &BlockLimitFunction::NO_LIMIT_HIT, + None, + ); + + let err = match tx_result { + TransactionResult::Success(tx_result) => { + txs_receipts.push(tx_result.receipt); + Ok(()) + } + TransactionResult::ProcessingError(e) => { + Err(format!("Error processing tx {}: {}", i, e.error)) + } + TransactionResult::Skipped(e) => Err(format!("Skipped tx {}: {}", i, e.error)), + TransactionResult::Problematic(e) => { + Err(format!("Problematic tx {}: {}", i, e.error)) + } + }; + if let Err(reason) = err { + let txid = tx.txid(); + return Err(ChainError::InvalidStacksTransaction( + format!("Unable to simulate transaction {txid}: {reason}").into(), + false, + )); + } + + block_fees += tx.get_tx_fee() as u128; + } + + let simulated_block = builder.mine_nakamoto_block(&mut tenure_tx, burn_chain_height); + + tenure_tx.rollback_block(); + + let tx_merkle_root = block.header.tx_merkle_root.clone(); + + let mut rpc_replayed_block = + RPCReplayedBlock::from_block(&simulated_block, block_fees, tenure_id, parent_block_id); + + for receipt in &txs_receipts { + let profiler_result = BlockReplayProfilerResult::default(); + let transaction = RPCReplayedBlockTransaction::from_receipt(receipt, &profiler_result); + rpc_replayed_block.transactions.push(transaction); + } + + Ok(rpc_replayed_block) + } +} + +/// Decode the HTTP request +impl HttpRequest for RPCNakamotoBlockSimulateRequestHandler { + fn verb(&self) -> &'static str { + "POST" + } + + fn path_regex(&self) -> Regex { + Regex::new(r#"^/v3/blocks/simulate/(?P[0-9a-f]{64})$"#).unwrap() + } + + fn metrics_identifier(&self) -> &str { + "/v3/blocks/simulate/:block_id" + } + + /// Try to decode this request. + /// There's nothing to load here, so just make sure the request is well-formed. + fn try_parse_request( + &mut self, + preamble: &HttpRequestPreamble, + captures: &Captures, + query: Option<&str>, + body: &[u8], + ) -> Result { + // If no authorization is set, then the block replay endpoint is not enabled + let Some(password) = &self.auth else { + return Err(Error::Http(400, "Bad Request.".into())); + }; + let Some(auth_header) = preamble.headers.get("authorization") else { + return Err(Error::Http(401, "Unauthorized".into())); + }; + if auth_header != password { + return Err(Error::Http(401, "Unauthorized".into())); + } + + let block_id_str = captures + .name("block_id") + .ok_or_else(|| { + Error::DecodeError("Failed to match path to block ID group".to_string()) + })? + .as_str(); + + let block_id = StacksBlockId::from_hex(block_id_str) + .map_err(|_| Error::DecodeError("Invalid path: unparseable block id".to_string()))?; + + self.block_id = Some(block_id); + + if let Some(query_string) = query { + for (key, value) in form_urlencoded::parse(query_string.as_bytes()) { + if key == "profiler" { + if value == "1" { + self.profiler = true; + } + break; + } + } + } + + if preamble.get_content_length() == 0 { + return Err(Error::DecodeError( + "Invalid Http request: expected non-zero-length body for block proposal endpoint" + .to_string(), + )); + } + if preamble.get_content_length() > MAX_PAYLOAD_LEN { + return Err(Error::DecodeError( + "Invalid Http request: BlockProposal body is too big".to_string(), + )); + } + + self.transactions = match preamble.content_type { + Some(HttpContentType::JSON) => Self::parse_json(body)?, + Some(_) => { + return Err(Error::DecodeError( + "Wrong Content-Type for block proposal; expected application/json".to_string(), + )) + } + None => { + return Err(Error::DecodeError( + "Missing Content-Type for block simulation".to_string(), + )) + } + }; + + Ok(HttpRequestContents::new().query_string(query)) + } +} + +impl RPCRequestHandler for RPCNakamotoBlockSimulateRequestHandler { + /// Reset internal state + fn restart(&mut self) { + self.block_id = None; + } + + /// Make the response + fn try_handle_request( + &mut self, + preamble: HttpRequestPreamble, + _contents: HttpRequestContents, + node: &mut StacksNodeState, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + let Some(block_id) = &self.block_id else { + return Err(NetError::SendError("Missing `block_id`".into())); + }; + + let simulated_block_res = + node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| { + self.block_simulate(sortdb, chainstate) + }); + + // start loading up the block + let simulated_block = match simulated_block_res { + Ok(simulated_block) => simulated_block, + Err(ChainError::NoSuchBlockError) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpNotFound::new(format!("No such block {block_id}\n")), + ) + .try_into_contents() + .map_err(NetError::from) + } + Err(e) => { + // nope -- error trying to check + let msg = format!("Failed to simulate block {}: {:?}\n", &block_id, &e); + warn!("{}", &msg); + return StacksHttpResponse::new_error(&preamble, &HttpServerError::new(msg)) + .try_into_contents() + .map_err(NetError::from); + } + }; + + let preamble = HttpResponsePreamble::ok_json(&preamble); + let body = HttpResponseContents::try_from_json(&simulated_block)?; + Ok((preamble, body)) + } +} + +impl StacksHttpRequest { + /// Make a new block_replay request to this endpoint + pub fn new_block_simulate(host: PeerHost, block_id: &StacksBlockId) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v3/blocks/simulate/{block_id}"), + HttpRequestContents::new(), + ) + .expect("FATAL: failed to construct request from infallible data") + } + + pub fn new_block_simulate_with_profiler( + host: PeerHost, + block_id: &StacksBlockId, + profiler: bool, + ) -> StacksHttpRequest { + StacksHttpRequest::new_for_peer( + host, + "GET".into(), + format!("/v3/blocks/simulate/{block_id}"), + HttpRequestContents::new().query_arg( + "profiler".into(), + if profiler { "1".into() } else { "0".into() }, + ), + ) + .expect("FATAL: failed to construct request from infallible data") + } +} + +/// Decode the HTTP response +impl HttpResponse for RPCNakamotoBlockSimulateRequestHandler { + /// Decode this response from a byte stream. This is called by the client to decode this + /// message + fn try_parse_response( + &self, + preamble: &HttpResponsePreamble, + body: &[u8], + ) -> Result { + let rpc_replayed_block: RPCReplayedBlock = parse_json(preamble, body)?; + Ok(HttpResponsePayload::try_from_json(rpc_replayed_block)?) + } +} + +impl StacksHttpResponse { + pub fn decode_simulated_block(self) -> Result { + let contents = self.get_http_payload_ok()?; + let response_json: serde_json::Value = contents.try_into()?; + let replayed_block: RPCReplayedBlock = serde_json::from_value(response_json) + .map_err(|_e| Error::DecodeError("Failed to decode JSON".to_string()))?; + Ok(replayed_block) + } +} diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index a5777a751d..3661d48a31 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -18,6 +18,7 @@ use crate::net::httpcore::StacksHttp; use crate::net::Error as NetError; pub mod blockreplay; +pub mod blocksimulate; pub mod callreadonly; pub mod fastcallreadonly; pub mod get_tenures_fork_info; @@ -78,6 +79,9 @@ impl StacksHttp { self.register_rpc_endpoint(blockreplay::RPCNakamotoBlockReplayRequestHandler::new( self.auth_token.clone(), )); + self.register_rpc_endpoint(blocksimulate::RPCNakamotoBlockSimulateRequestHandler::new( + self.auth_token.clone(), + )); self.register_rpc_endpoint(callreadonly::RPCCallReadOnlyRequestHandler::new( self.maximum_call_argument_size, self.read_only_call_limit.clone(), diff --git a/stackslib/src/net/api/tests/blockreplay.rs b/stackslib/src/net/api/tests/blockreplay.rs index c4ae3d0ea0..9bc385c0fb 100644 --- a/stackslib/src/net/api/tests/blockreplay.rs +++ b/stackslib/src/net/api/tests/blockreplay.rs @@ -72,6 +72,43 @@ fn test_try_parse_request() { let (preamble, contents) = parsed_request.destruct(); assert_eq!(&preamble, request.preamble()); + assert_eq!(handler.profiler, false); +} + +#[test] +fn test_try_parse_request_with_profiler() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default()); + + let mut request = StacksHttpRequest::new_block_replay_with_profiler( + addr.into(), + &StacksBlockId([0x01; 32]), + true, + ); + + // add the authorization header + request.add_header("authorization".into(), "password".into()); + + let bytes = request.try_serialize().unwrap(); + + debug!("Request:\n{}\n", std::str::from_utf8(&bytes).unwrap()); + + let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap(); + + let mut handler = + blockreplay::RPCNakamotoBlockReplayRequestHandler::new(Some("password".into())); + + let parsed_request = http + .handle_try_parse_request( + &mut handler, + &parsed_preamble.expect_request(), + &bytes[offset..], + ) + .unwrap(); + + let (preamble, contents) = parsed_request.destruct(); + + assert_eq!(handler.profiler, true); } #[test] @@ -110,8 +147,11 @@ fn test_try_make_response() { let mut requests = vec![]; // query existing, non-empty Nakamoto block - let mut request = - StacksHttpRequest::new_block_replay(addr.clone().into(), &rpc_test.canonical_tip); + let mut request = StacksHttpRequest::new_block_replay_with_profiler( + addr.clone().into(), + &rpc_test.canonical_tip, + true, + ); // add the authorization header request.add_header("authorization".into(), "password".into()); requests.push(request);