Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contrib/stacks-inspect/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ stackslib = { package = "stackslib", path = "../../stackslib", default-features
clarity = { path = "../../clarity", default-features = false }
libstackerdb = { path = "../../libstackerdb", default-features = false }
stacks-common = { path = "../../stacks-common", default-features = false }
regex = { version = "1", default-features = false }
rusqlite = { workspace = true }
serde_json = { workspace = true }
slog = { workspace = true }
Expand Down
8 changes: 4 additions & 4 deletions contrib/stacks-inspect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ cargo build -p stacks-inspect
Basic usage:
```bash
# Show version
./target/debug/stacks-inspect --version
cargo run -p stacks-inspect -- --version

# Example: decode a bitcoin header from file
./target/debug/stacks-inspect decode-bitcoin-header <HEIGHT> <PATH>
cargo run -p stacks-inspect -- decode-bitcoin-header <HEIGHT> <PATH>

# Example: analyze anti-MEV behavior over a height range
./target/debug/stacks-inspect analyze-sortition-mev <burn_db> <sort_db> <chainstate_db> <start> <end> [miner advantage ...]
cargo run -p stacks-inspect -- analyze-sortition-mev <burn_db> <sort_db> <chainstate_db> <start> <end> [miner advantage ...]
```

For detailed commands and flags, run:
```bash
./target/debug/stacks-inspect --help
cargo run -p stacks-inspect -- --help
```

Notes:
Expand Down
136 changes: 77 additions & 59 deletions stackslib/src/cli.rs → contrib/stacks-inspect/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020 Stacks Open Internet Foundation
// 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
Expand All @@ -14,41 +13,42 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

//! Subcommands used by `stacks-inspect` binary

use std::path::PathBuf;
use std::time::Instant;
use std::{fs, process};

use clarity::types::chainstate::SortitionId;
use clarity::util::hash::{to_hex, Sha512Trunc256Sum};
use db::blocks::DummyEventDispatcher;
use db::ChainstateTx;
use clarity::util::hash::{Sha512Trunc256Sum, to_hex};
use regex::Regex;
use rusqlite::{Connection, OpenFlags};
use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId};
use stacks_common::types::sqlite::NO_PARAMS;
use stacks_common::util::hash::Hash160;
use stacks_common::util::vrf::VRFProof;

use crate::burnchains::Burnchain;
use crate::chainstate::burn::db::sortdb::{
get_ancestor_sort_id, SortitionDB, SortitionHandleContext,
use stacks_common::{debug, info, warn};
use stackslib::burnchains::Burnchain;
use stackslib::chainstate::burn::ConsensusHash;
use stackslib::chainstate::burn::db::sortdb::{
SortitionDB, SortitionHandleContext, get_ancestor_sort_id,
};
use stackslib::chainstate::coordinator::OnChainRewardSetProvider;
use stackslib::chainstate::nakamoto::miner::{
BlockMetadata, NakamotoBlockBuilder, NakamotoTenureInfo,
};
use stackslib::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState};
use stackslib::chainstate::stacks::db::blocks::DummyEventDispatcher;
use stackslib::chainstate::stacks::db::{
ChainstateTx, StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo,
};
use crate::chainstate::burn::ConsensusHash;
use crate::chainstate::coordinator::OnChainRewardSetProvider;
use crate::chainstate::nakamoto::miner::{BlockMetadata, NakamotoBlockBuilder, NakamotoTenureInfo};
use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState};
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo};
use crate::chainstate::stacks::miner::*;
use crate::chainstate::stacks::{Error as ChainstateError, *};
use crate::clarity_vm::clarity::ClarityInstance;
use crate::clarity_vm::database::GetTenureStartId;
use crate::config::{Config, ConfigFile, DEFAULT_MAINNET_CONFIG};
use crate::core::*;
use crate::cost_estimates::metrics::UnitMetric;
use crate::cost_estimates::UnitEstimator;
use crate::util_lib::db::IndexDBTx;
use stackslib::chainstate::stacks::miner::*;
use stackslib::chainstate::stacks::{Error as ChainstateError, *};
use stackslib::clarity_vm::clarity::ClarityInstance;
use stackslib::clarity_vm::database::GetTenureStartId;
use stackslib::config::{Config, ConfigFile, DEFAULT_MAINNET_CONFIG};
use stackslib::core::*;
use stackslib::cost_estimates::UnitEstimator;
use stackslib::cost_estimates::metrics::UnitMetric;
use stackslib::util_lib::db::IndexDBTx;

/// Options common to many `stacks-inspect` subcommands
/// Returned by `process_common_opts()`
Expand Down Expand Up @@ -141,34 +141,38 @@ pub fn command_replay_block(argv: &[String], conf: Option<&Config>) {

let query = match mode {
Some("prefix") => format!(
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
argv[3]
),
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
argv[3]
),
Some("first") => format!(
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
argv[3]
),
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
argv[3]
),
Some("range") => {
let arg4 = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let arg5 = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let start = arg4.saturating_sub(1);
let blocks = arg5.saturating_sub(arg4);
format!("SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}")
format!(
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}"
)
}
Some("index-range") => {
let start = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let end = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let blocks = end.saturating_sub(start);
format!("SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}")
format!(
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}"
)
}
Some("last") => format!(
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
argv[3]
),
"SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
argv[3]
),
Some(_) => print_help_and_exit(),
// Default to ALL blocks
None => "SELECT index_block_hash FROM staging_blocks WHERE orphaned = 0".into(),
Expand Down Expand Up @@ -229,34 +233,38 @@ pub fn command_replay_block_nakamoto(argv: &[String], conf: Option<&Config>) {

let query = match mode {
Some("prefix") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
argv[3]
),
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 AND index_block_hash LIKE \"{}%\"",
argv[3]
),
Some("first") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
argv[3]
),
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {}",
argv[3]
),
Some("range") => {
let arg4 = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let arg5 = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let start = arg4.saturating_sub(1);
let blocks = arg5.saturating_sub(arg4);
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}")
format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height ASC LIMIT {start}, {blocks}"
)
}
Some("index-range") => {
let start = argv[3]
.parse::<u64>()
.expect("<start_block> not a valid u64");
let end = argv[4].parse::<u64>().expect("<end-block> not a valid u64");
let blocks = end.saturating_sub(start);
format!("SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}")
format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY index_block_hash ASC LIMIT {start}, {blocks}"
)
}
Some("last") => format!(
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
argv[3]
),
"SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0 ORDER BY height DESC LIMIT {}",
argv[3]
),
Some(_) => print_help_and_exit(),
// Default to ALL blocks
None => "SELECT index_block_hash FROM nakamoto_staging_blocks WHERE orphaned = 0".into(),
Expand Down Expand Up @@ -386,8 +394,12 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) {
let n = &argv[0];
eprintln!("Usage: {n} <working-dir> [min-fee [max-time]]");
eprintln!("");
eprintln!("Given a <working-dir>, try to ''mine'' an anchored block. This invokes the miner block");
eprintln!("assembly, but does not attempt to broadcast a block commit. This is useful for determining");
eprintln!(
"Given a <working-dir>, try to ''mine'' an anchored block. This invokes the miner block"
);
eprintln!(
"assembly, but does not attempt to broadcast a block commit. This is useful for determining"
);
eprintln!("what transactions a given chain state would include in an anchor block,");
eprintln!("or otherwise simulating a miner.");
process::exit(1);
Expand Down Expand Up @@ -523,11 +535,11 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) {
let elapsed = start.elapsed();
let summary = format!(
"block @ height = {h} off of {pid} ({pch}/{pbh}) in {t}ms. Min-fee: {min_fee}, Max-time: {max_time}",
h=parent_stacks_header.stacks_block_height + 1,
pid=&parent_stacks_header.index_block_hash(),
pch=&parent_stacks_header.consensus_hash,
pbh=&parent_stacks_header.anchored_header.block_hash(),
t=elapsed.as_millis(),
h = parent_stacks_header.stacks_block_height + 1,
pid = &parent_stacks_header.index_block_hash(),
pch = &parent_stacks_header.consensus_hash,
pbh = &parent_stacks_header.anchored_header.block_hash(),
t = elapsed.as_millis(),
);

let code = match result {
Expand Down Expand Up @@ -770,7 +782,9 @@ fn replay_block(
),
None => {
// shouldn't happen
panic!("CORRUPTION: staging block {block_consensus_hash}/{block_hash} does not correspond to a burn block");
panic!(
"CORRUPTION: staging block {block_consensus_hash}/{block_hash} does not correspond to a burn block"
);
}
};

Expand Down Expand Up @@ -835,8 +849,10 @@ fn replay_block(
) {
Ok((receipt, _, _)) => {
if receipt.anchored_block_cost != cost {
println!("Failed processing block! block = {block_id}. Unexpected cost. expected = {cost}, evaluated = {}",
receipt.anchored_block_cost);
println!(
"Failed processing block! block = {block_id}. Unexpected cost. expected = {cost}, evaluated = {}",
receipt.anchored_block_cost
);
process::exit(1);
}

Expand Down Expand Up @@ -1153,7 +1169,9 @@ fn replay_block_nakamoto(
// check the cost
let evaluated_cost = receipt.anchored_block_cost.clone();
if evaluated_cost != expected_cost {
println!("Failed processing block! block = {block_id}. Unexpected cost. expected = {expected_cost}, evaluated = {evaluated_cost}");
println!(
"Failed processing block! block = {block_id}. Unexpected cost. expected = {expected_cost}, evaluated = {evaluated_cost}"
);
process::exit(1);
}
}
Expand Down Expand Up @@ -1197,7 +1215,7 @@ pub mod test {
"stacks-inspect try-mine --config my_config.toml /tmp/chainstate/mainnet",
);
let argv_init = argv.clone();
let opts = drain_common_opts(&mut argv, 0);
let _opts = drain_common_opts(&mut argv, 0);
let opts = drain_common_opts(&mut argv, 1);

assert_eq!(argv, argv_init);
Expand Down
21 changes: 13 additions & 8 deletions contrib/stacks-inspect/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ extern crate stacks_common;
use clarity::consts::CHAIN_ID_MAINNET;
use clarity::types::StacksEpochId;
use clarity::types::chainstate::StacksPrivateKey;
use clarity_cli::DEFAULT_CLI_EPOCH;
use stacks_inspect::{
command_contract_hash, command_replay_block, command_replay_block_nakamoto,
command_replay_mock_mining, command_try_mine, drain_common_opts,
};
use stackslib::chainstate::stacks::miner::BlockBuilderSettings;
use stackslib::chainstate::stacks::{
CoinbasePayload, StacksBlock, StacksBlockBuilder, StacksMicroblock, StacksTransaction,
Expand Down Expand Up @@ -80,6 +85,7 @@ use stackslib::chainstate::stacks::index::marf::{MARF, MARFOpenOpts, MarfConnect
use stackslib::clarity::vm::ClarityVersion;
use stackslib::clarity::vm::costs::ExecutionCost;
use stackslib::clarity::vm::types::StacksAddressExtensions;
use stackslib::clarity_cli;
use stackslib::core::MemPoolDB;
use stackslib::cost_estimates::UnitEstimator;
use stackslib::cost_estimates::metrics::UnitMetric;
Expand All @@ -91,7 +97,6 @@ use stackslib::net::relay::Relayer;
use stackslib::net::{GetNakamotoInvData, HandshakeData, StacksMessage, StacksMessageType};
use stackslib::util_lib::db::sqlite_open;
use stackslib::util_lib::strings::UrlString;
use stackslib::{clarity_cli, cli};

struct P2PSession {
pub local_peer: LocalPeer,
Expand Down Expand Up @@ -301,7 +306,7 @@ fn main() {
process::exit(1);
}

let common_opts = cli::drain_common_opts(&mut argv, 1);
let common_opts = drain_common_opts(&mut argv, 1);

if argv[1] == "--version" {
println!(
Expand Down Expand Up @@ -789,7 +794,7 @@ check if the associated microblocks can be downloaded
}

if argv[1] == "try-mine" {
cli::command_try_mine(&argv[1..], common_opts.config.as_ref());
command_try_mine(&argv[1..], common_opts.config.as_ref());
process::exit(0);
}

Expand Down Expand Up @@ -896,7 +901,7 @@ check if the associated microblocks can be downloaded
}
let program: String = fs::read_to_string(&argv[2])
.unwrap_or_else(|_| panic!("Error reading file: {}", argv[2]));
let clarity_version = ClarityVersion::default_for_epoch(clarity_cli::DEFAULT_CLI_EPOCH);
let clarity_version = ClarityVersion::default_for_epoch(DEFAULT_CLI_EPOCH);
match clarity_cli::vm_execute(&program, clarity_version) {
Ok(Some(result)) => println!("{result}"),
Ok(None) => println!(),
Expand Down Expand Up @@ -1582,17 +1587,17 @@ check if the associated microblocks can be downloaded
}

if argv[1] == "replay-block" {
cli::command_replay_block(&argv[1..], common_opts.config.as_ref());
command_replay_block(&argv[1..], common_opts.config.as_ref());
process::exit(0);
}

if argv[1] == "replay-naka-block" {
cli::command_replay_block_nakamoto(&argv[1..], common_opts.config.as_ref());
command_replay_block_nakamoto(&argv[1..], common_opts.config.as_ref());
process::exit(0);
}

if argv[1] == "replay-mock-mining" {
cli::command_replay_mock_mining(&argv[1..], common_opts.config.as_ref());
command_replay_mock_mining(&argv[1..], common_opts.config.as_ref());
process::exit(0);
}

Expand All @@ -1601,7 +1606,7 @@ check if the associated microblocks can be downloaded
}

if argv[1] == "contract-hash" {
cli::command_contract_hash(&argv[1..], common_opts.config.as_ref());
command_contract_hash(&argv[1..], common_opts.config.as_ref());
process::exit(0);
}

Expand Down
1 change: 1 addition & 0 deletions stacks-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ clarity = { path = "../clarity", features = ["default", "testing"]}
stacks-common = { path = "../stacks-common", features = ["default", "testing"] }
stacks = { package = "stackslib", path = "../stackslib", features = ["default", "testing"] }
stacks-signer = { path = "../stacks-signer", features = ["testing"] }
stacks-inspect = { path = "../contrib/stacks-inspect", default-features = false }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
mutants = "0.0.3"
Expand Down
Loading