diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md index 94f60f52fb..436f523b43 100644 --- a/stacks-signer/CHANGELOG.md +++ b/stacks-signer/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Introduced the `reorg_attempts_activity_timeout_ms` configuration option for signers which is used to determine the length of time after the last block of a tenure is confirmed that an incoming miner's attempts to reorg it are considered valid miner activity. - Add signer configuration option `tenure_idle_timeout_buffer_secs` to specify the number of seconds of buffer the signer will add to its tenure extend time that it sends to miners. The idea is to allow for some clock skew between the miner and signers, preventing the case where the miner attempts to tenure extend too early. +- Add `txindex` configuration option enabling the storage (and querying via api) of transactions. Note: the old STACKS_TRANSACTION_LOG environment var configuration is no more available. ### Changed diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 2ea0fcb9ca..311a104825 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -208,6 +208,9 @@ pub struct ChainsCoordinatorConfig { /// true: always wait for canonical anchor blocks, even if it stalls the chain /// false: proceed to process new chain history even if we're missing an anchor block. pub require_affirmed_anchor_blocks: bool, + /// true: enable transactions indexing + /// false: no transactions indexing + pub txindex: bool, } impl ChainsCoordinatorConfig { @@ -216,6 +219,7 @@ impl ChainsCoordinatorConfig { always_use_affirmation_maps: true, require_affirmed_anchor_blocks: true, assume_present_anchor_blocks: true, + txindex: false, } } @@ -224,6 +228,7 @@ impl ChainsCoordinatorConfig { always_use_affirmation_maps: false, require_affirmed_anchor_blocks: false, assume_present_anchor_blocks: false, + txindex: false, } } } @@ -249,7 +254,7 @@ pub struct ChainsCoordinator< pub reward_set_provider: R, pub notifier: N, pub atlas_config: AtlasConfig, - config: ChainsCoordinatorConfig, + pub config: ChainsCoordinatorConfig, burnchain_indexer: B, /// Used to tell the P2P thread that the stackerdb /// needs to be refreshed. diff --git a/stackslib/src/chainstate/nakamoto/coordinator/mod.rs b/stackslib/src/chainstate/nakamoto/coordinator/mod.rs index f700f32702..1110dd7857 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/mod.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/mod.rs @@ -801,6 +801,7 @@ impl< &mut self.sortition_db, &canonical_sortition_tip, self.dispatcher, + self.config.txindex, ) { Ok(receipt_opt) => receipt_opt, Err(ChainstateError::InvalidStacksBlock(msg)) => { diff --git a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs index 72ebc8ff07..869c009275 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs @@ -34,7 +34,7 @@ use stacks_common::types::chainstate::{ BurnchainHeaderHash, StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey, }; use stacks_common::types::{Address, StacksEpoch, StacksEpochId, StacksPublicKeyBuffer}; -use stacks_common::util::hash::Hash160; +use stacks_common::util::hash::{to_hex, Hash160}; use stacks_common::util::secp256k1::Secp256k1PrivateKey; use stacks_common::util::vrf::VRFProof; @@ -1429,7 +1429,7 @@ fn pox_treatment() { false }, ); - let processing_result = peer.try_process_block(&invalid_block).unwrap_err(); + let processing_result = peer.try_process_block(&invalid_block, false).unwrap_err(); assert_eq!( processing_result.to_string(), "Bitvec does not match the block commit's PoX handling".to_string(), @@ -1520,7 +1520,7 @@ fn pox_treatment() { false }, ); - let processing_result = peer.try_process_block(&invalid_block).unwrap_err(); + let processing_result = peer.try_process_block(&invalid_block, false).unwrap_err(); assert_eq!( processing_result.to_string(), "Bitvec does not match the block commit's PoX handling".to_string(), @@ -1568,6 +1568,97 @@ fn pox_treatment() { ); } +#[test] +// Test Transactions indexing system +fn transactions_indexing() { + let private_key = StacksPrivateKey::from_seed(&[2]); + let addr = StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&private_key)); + + let num_stackers: u32 = 4; + let mut signing_key_seed = num_stackers.to_be_bytes().to_vec(); + signing_key_seed.extend_from_slice(&[1, 1, 1, 1]); + let signing_key = StacksPrivateKey::from_seed(signing_key_seed.as_slice()); + let test_stackers = (0..num_stackers) + .map(|index| TestStacker { + signer_private_key: signing_key.clone(), + stacker_private_key: StacksPrivateKey::from_seed(&index.to_be_bytes()), + amount: u64::MAX as u128 - 10000, + pox_addr: Some(PoxAddress::Standard( + StacksAddress::new( + C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + Hash160::from_data(&index.to_be_bytes()), + ) + .unwrap(), + Some(AddressHashMode::SerializeP2PKH), + )), + max_amount: None, + }) + .collect::>(); + let test_signers = TestSigners::new(vec![signing_key]); + let mut pox_constants = TestPeerConfig::default().burnchain.pox_constants; + pox_constants.reward_cycle_length = 10; + pox_constants.v2_unlock_height = 21; + pox_constants.pox_3_activation_height = 26; + pox_constants.v3_unlock_height = 27; + pox_constants.pox_4_activation_height = 28; + + let mut boot_plan = NakamotoBootPlan::new(function_name!()) + .with_test_stackers(test_stackers.clone()) + .with_test_signers(test_signers.clone()) + .with_private_key(private_key); + boot_plan.pox_constants = pox_constants; + + let mut peer = boot_plan.boot_into_nakamoto_peer(vec![], None); + + // generate a new block with txindex + let (tracked_block, burn_height, ..) = + peer.single_block_tenure(&private_key, |_| {}, |_| {}, |_| false); + + assert_eq!(peer.try_process_block(&tracked_block, true).unwrap(), true); + + let tracked_block_id = tracked_block.block_id(); + + // generate a new block but without txindex + let (untracked_block, burn_height, ..) = + peer.single_block_tenure(&private_key, |_| {}, |_| {}, |_| false); + + assert_eq!( + peer.try_process_block(&untracked_block, false).unwrap(), + true + ); + + let untracked_block_id = untracked_block.block_id(); + + let chainstate = &peer.stacks_node.unwrap().chainstate; + + // compare transactions to what has been tracked + for tx in tracked_block.txs { + let current_tx_hex = to_hex(&tx.serialize_to_vec()); + let (index_block_hash, tx_hex) = + NakamotoChainState::get_index_block_hash_and_tx_hex_from_txid( + &chainstate.index_conn(), + tx.txid(), + ) + .unwrap() + .unwrap(); + assert_eq!(index_block_hash, tracked_block_id); + assert_eq!(tx_hex, current_tx_hex); + } + + // ensure untracked transactions are not recorded + for tx in untracked_block.txs { + assert_eq!( + NakamotoChainState::get_index_block_hash_and_tx_hex_from_txid( + &chainstate.index_conn(), + tx.txid(), + ) + .unwrap() + .is_none(), + true + ); + } +} + /// Test chainstate getters against an instantiated epoch2/Nakamoto chain. /// There are 11 epoch2 blocks and 2 nakamto tenure with 10 nakamoto blocks each /// Tests: diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 056bd53fe4..b4109ea81b 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -1854,6 +1854,7 @@ impl NakamotoChainState { sort_db: &mut SortitionDB, canonical_sortition_tip: &SortitionId, dispatcher_opt: Option<&T>, + txindex: bool, ) -> Result, ChainstateError> { #[cfg(test)] fault_injection::stall_block_processing(); @@ -2147,6 +2148,20 @@ impl NakamotoChainState { tx_receipts.push(unlock_receipt); } + // if txindex is enabled, let's record each transaction in the transactions table + if txindex { + let block_id = next_ready_block.block_id(); + let stacks_db_tx = stacks_chain_state.index_tx_begin(); + for tx_receipt in tx_receipts.iter() { + Self::record_transaction(&stacks_db_tx, &block_id, tx_receipt); + } + + let commit = stacks_db_tx.commit(); + if commit.is_err() { + warn!("Could not index transactions: {}", commit.err().unwrap()); + } + } + // announce the block, if we're connected to an event dispatcher if let Some(dispatcher) = dispatcher_opt { let block_event = ( @@ -3519,6 +3534,42 @@ impl NakamotoChainState { Ok(()) } + // Index a transaction in the transactions table (used by txindex) + pub fn record_transaction( + stacks_db_tx: &StacksDBTx, + block_id: &StacksBlockId, + tx_receipt: &StacksTransactionReceipt, + ) { + let insert = + "INSERT INTO transactions (txid, index_block_hash, tx_hex, result) VALUES (?, ?, ?, ?)"; + let txid = tx_receipt.transaction.txid(); + let tx_hex = tx_receipt.transaction.serialize_to_dbstring(); + let result = tx_receipt.result.to_string(); + let params = params![txid, block_id, tx_hex, result]; + if let Err(e) = stacks_db_tx.execute(insert, params) { + warn!("Failed to record TX: {}", e); + } + } + + // Get index_block_hash and transaction payload hex by txid from the transactions table + pub fn get_index_block_hash_and_tx_hex_from_txid( + conn: &Connection, + txid: Txid, + ) -> Result, ChainstateError> { + let sql = "SELECT index_block_hash, tx_hex FROM transactions WHERE txid = ?"; + let args = params![txid]; + + let mut stmt = conn.prepare(sql)?; + Ok(stmt + .query_row(args, |row| { + let index_block_hash: StacksBlockId = row.get(0)?; + let tx_hex: String = row.get(1)?; + + Ok((index_block_hash, tx_hex)) + }) + .optional()?) + } + /// Fetch number of blocks signed for a given signer and reward cycle /// This is the data tracked by `record_block_signers()` pub fn get_signer_block_count( @@ -4642,7 +4693,7 @@ impl NakamotoChainState { .expect("FATAL: failed to advance chain tip"); let new_block_id = new_tip.index_block_hash(); - chainstate_tx.log_transactions_processed(&new_block_id, &tx_receipts); + chainstate_tx.log_transactions_processed(&tx_receipts); let reward_cycle = pox_constants .block_height_to_reward_cycle(first_block_height, chain_tip_burn_header_height.into()); diff --git a/stackslib/src/chainstate/nakamoto/shadow.rs b/stackslib/src/chainstate/nakamoto/shadow.rs index 6b00e9ac40..ce2b84057c 100644 --- a/stackslib/src/chainstate/nakamoto/shadow.rs +++ b/stackslib/src/chainstate/nakamoto/shadow.rs @@ -897,6 +897,7 @@ pub fn process_shadow_block( sort_db, &sort_tip.sortition_id, no_dispatch.as_ref(), + false, ) { Ok(receipt_opt) => receipt_opt, Err(ChainstateError::InvalidStacksBlock(msg)) => { diff --git a/stackslib/src/chainstate/nakamoto/tests/node.rs b/stackslib/src/chainstate/nakamoto/tests/node.rs index 783e07901a..bb9a22c997 100644 --- a/stackslib/src/chainstate/nakamoto/tests/node.rs +++ b/stackslib/src/chainstate/nakamoto/tests/node.rs @@ -1377,7 +1377,11 @@ impl TestPeer<'_> { proof } - pub fn try_process_block(&mut self, block: &NakamotoBlock) -> Result { + pub fn try_process_block( + &mut self, + block: &NakamotoBlock, + txindex: bool, + ) -> Result { let mut sort_handle = self.sortdb.as_ref().unwrap().index_handle_at_tip(); let stacks_tip = sort_handle.get_nakamoto_tip_block_id().unwrap().unwrap(); let accepted = Relayer::process_new_nakamoto_block( @@ -1400,6 +1404,7 @@ impl TestPeer<'_> { self.sortdb.as_mut().unwrap(), &sort_tip, None, + txindex, )? else { return Ok(false); diff --git a/stackslib/src/chainstate/stacks/db/blocks.rs b/stackslib/src/chainstate/stacks/db/blocks.rs index 8e6c0da9de..126e19657f 100644 --- a/stackslib/src/chainstate/stacks/db/blocks.rs +++ b/stackslib/src/chainstate/stacks/db/blocks.rs @@ -5847,7 +5847,7 @@ impl StacksChainState { ) .expect("FATAL: failed to advance chain tip"); - chainstate_tx.log_transactions_processed(&new_tip.index_block_hash(), &tx_receipts); + chainstate_tx.log_transactions_processed(&tx_receipts); // store the reward set calculated during this block if it happened // NOTE: miner and proposal evaluation should not invoke this because diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index 14fece138e..63bfae79ad 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -97,11 +97,6 @@ pub mod headers; pub mod transactions; pub mod unconfirmed; -lazy_static! { - pub static ref TRANSACTION_LOG: bool = - std::env::var("STACKS_TRANSACTION_LOG") == Ok("1".into()); -} - /// Fault injection struct for various kinds of faults we'd like to introduce into the system pub struct StacksChainStateFaults { // if true, then the envar STACKS_HIDE_BLOCKS_AT_HEIGHT will be consulted to get a list of @@ -636,24 +631,7 @@ impl<'a> ChainstateTx<'a> { &self.config } - pub fn log_transactions_processed( - &self, - block_id: &StacksBlockId, - events: &[StacksTransactionReceipt], - ) { - if *TRANSACTION_LOG { - let insert = - "INSERT INTO transactions (txid, index_block_hash, tx_hex, result) VALUES (?, ?, ?, ?)"; - for tx_event in events.iter() { - let txid = tx_event.transaction.txid(); - let tx_hex = tx_event.transaction.serialize_to_dbstring(); - let result = tx_event.result.to_string(); - let params = params![txid, block_id, tx_hex, result]; - if let Err(e) = self.tx.tx().execute(insert, params) { - warn!("Failed to log TX: {}", e); - } - } - } + pub fn log_transactions_processed(&self, events: &[StacksTransactionReceipt]) { for tx_event in events.iter() { let txid = tx_event.transaction.txid(); if let Err(e) = monitoring::log_transaction_processed(&txid, &self.root_path) { diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index a01be4cc41..5384ea2fe2 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -1691,6 +1691,8 @@ pub struct NodeConfig { pub chain_liveness_poll_time_secs: u64, /// stacker DBs we replicate pub stacker_dbs: Vec, + /// enable transactions indexing + pub txindex: bool, } #[derive(Clone, Debug, Default)] @@ -1954,6 +1956,7 @@ impl Default for NodeConfig { fault_injection_hide_blocks: false, chain_liveness_poll_time_secs: 300, stacker_dbs: vec![], + txindex: false, } } } @@ -2472,6 +2475,8 @@ pub struct NodeConfigFile { pub stacker_dbs: Option>, /// fault injection: fail to push blocks with this probability (0-100) pub fault_injection_block_push_fail_probability: Option, + /// enable transactions indexing + pub txindex: bool, } impl NodeConfigFile { @@ -2570,6 +2575,8 @@ impl NodeConfigFile { } else { default_node_config.fault_injection_block_push_fail_probability }, + + txindex: self.txindex, }; Ok(node_config) } diff --git a/testnet/stacks-node/src/run_loop/nakamoto.rs b/testnet/stacks-node/src/run_loop/nakamoto.rs index 335fb325d8..2a2bb639ac 100644 --- a/testnet/stacks-node/src/run_loop/nakamoto.rs +++ b/testnet/stacks-node/src/run_loop/nakamoto.rs @@ -324,6 +324,7 @@ impl RunLoop { require_affirmed_anchor_blocks: moved_config .node .require_affirmed_anchor_blocks, + txindex: moved_config.node.txindex, }; ChainsCoordinator::run( coord_config, diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index 299335f35f..0fe2cec772 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -657,6 +657,7 @@ impl RunLoop { require_affirmed_anchor_blocks: moved_config .node .require_affirmed_anchor_blocks, + txindex: moved_config.node.txindex, }; ChainsCoordinator::run( coord_config,