Skip to content
Open
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
4 changes: 2 additions & 2 deletions clarity/src/vm/analysis/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ fn test_write_attempt_in_readonly() {
fn test_run_analysis_aborts_when_deadline_already_elapsed() {
let err = utils::run_analysis_with_resource_limiter(
"(define-read-only (foo) (+ 1 1))",
ResourceBudget::new()
ResourceBudget::unlimited()
.with_max_duration(Some(Duration::ZERO))
.start_tracking(),
)
Expand Down Expand Up @@ -548,7 +548,7 @@ fn test_run_analysis_no_tracking_is_not_time_limited() {
fn test_run_analysis_generous_deadline_succeeds() {
let result = utils::run_analysis_with_resource_limiter(
"(define-read-only (foo) (+ 1 1))",
ResourceBudget::new()
ResourceBudget::unlimited()
.with_max_duration(Some(Duration::from_secs(300)))
.start_tracking(),
);
Expand Down
2 changes: 1 addition & 1 deletion clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4526,7 +4526,7 @@ fn test_clarity2_inner_type_check_type_aborts_when_deadline_elapsed() {
let mut db = marf.as_analysis_db();
let mut cost_tracker = LimitedCostTracker::new_free();
// A zero-duration deadline is already elapsed at the first check.
let resource_limiter = ResourceBudget::new()
let resource_limiter = ResourceBudget::unlimited()
.with_max_duration(Some(Duration::ZERO))
.start_tracking();

Expand Down
2 changes: 1 addition & 1 deletion clarity/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ pub fn execute_with_limited_execution_time(
false,
clarity_types::types::StandardPrincipalData::transient(),
|g| {
let budget = ResourceBudget::new().with_max_duration(Some(max_execution_time));
let budget = ResourceBudget::unlimited().with_max_duration(Some(max_execution_time));
g.set_execution_resource_limiter(budget.start_tracking());
Ok(())
},
Expand Down
16 changes: 3 additions & 13 deletions clarity/src/vm/resource_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,25 +168,21 @@ impl MemoryTracker {
///
/// During consensus-critical work, the budget MUST be [`ResourceBudget::unlimited`]
/// to ensure determinism.
#[derive(Debug, Clone, Copy)]
pub struct ResourceBudget {
max_duration: Option<Duration>,
max_allocated_bytes: Option<u64>,
}

impl ResourceBudget {
pub fn new() -> Self {
/// Creates a new instance with no configured budgets.
pub fn unlimited() -> Self {
Self {
max_duration: None,
max_allocated_bytes: None,
}
}

pub fn unlimited() -> Self {
// identical to Self::new(), but also provided under the name `unlimited`
// to make intentions obvious at the call site
Self::new()
}

pub fn with_max_duration(mut self, duration: Option<Duration>) -> Self {
self.max_duration = duration;
self
Expand All @@ -204,12 +200,6 @@ impl ResourceBudget {
}
}

impl Default for ResourceBudget {
fn default() -> Self {
Self::new()
}
}

pub enum ResourceLimitExceeded {
MaxDurationExceeded(String),
MaxAllocationExceeded(String),
Expand Down
61 changes: 61 additions & 0 deletions stacks-codec/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2535,6 +2535,20 @@ pub struct StacksMicroblockHeader {
pub signature: MessageSignature,
}

/// Signer relationship recovered from two valid microblock-header signatures.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MicroblockSignerMatch {
/// Both headers were signed by the same key.
Common(Hash160),
/// The headers were signed by different keys.
Different {
/// Signer recovered from the first header.
first: Hash160,
/// Signer recovered from the second header.
second: Hash160,
},
}

impl StacksMessageCodec for StacksMicroblockHeader {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), codec_error> {
self.serialize(fd, false)
Expand Down Expand Up @@ -2615,6 +2629,18 @@ impl StacksMicroblockHeader {
Ok(Hash160::from_node_public_key(&pubk))
}

/// Recovers and compares the signers of two microblock headers.
pub fn recover_signer_match(&self, other: &Self) -> Result<MicroblockSignerMatch, AuthError> {
let first = self.check_recover_pubkey()?;
let second = other.check_recover_pubkey()?;

Ok(if first == second {
MicroblockSignerMatch::Common(first)
} else {
MicroblockSignerMatch::Different { first, second }
})
}

pub fn verify(&self, pubk_hash: &Hash160) -> Result<(), AuthError> {
let pubkh = self.check_recover_pubkey()?;

Expand Down Expand Up @@ -4431,6 +4457,41 @@ mod tests {
assert_eq!(decoded, header);
}

#[test]
fn microblock_headers_recover_signer_match() {
let signer = StacksPrivateKey::random();
let other_signer = StacksPrivateKey::random();
let parent = BlockHeaderHash([0x77; 32]);

let mut first =
StacksMicroblockHeader::first_unsigned(&parent, &Sha512Trunc256Sum([0x11; 32]));
first.sign(&signer).unwrap();

let mut same_signer =
StacksMicroblockHeader::first_unsigned(&parent, &Sha512Trunc256Sum([0x22; 32]));
same_signer.sign(&signer).unwrap();

let mut different_signer =
StacksMicroblockHeader::first_unsigned(&parent, &Sha512Trunc256Sum([0x33; 32]));
different_signer.sign(&other_signer).unwrap();

let first_signer = first.check_recover_pubkey().unwrap();
assert_eq!(
first.recover_signer_match(&same_signer).unwrap(),
MicroblockSignerMatch::Common(first_signer.clone()),
);
assert_eq!(
first.recover_signer_match(&different_signer).unwrap(),
MicroblockSignerMatch::Different {
first: first_signer,
second: different_signer.check_recover_pubkey().unwrap(),
},
);

let unsigned = StacksMicroblockHeader::first_empty_unsigned(&parent);
assert!(first.recover_signer_match(&unsigned).is_err());
}

/// Every `TransactionAuthFlags` discriminant must serialize to a single
/// known byte.
#[test]
Expand Down
2 changes: 1 addition & 1 deletion stacks-node/src/nakamoto_node/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2226,7 +2226,7 @@ fn should_read_count_extend_units() {
burn_tip_at_start: ConsensusHash([0; 20]),
abort_flag: Arc::new(AtomicBool::new(false)),
reset_mempool_caches: false,
miner_db: MinerDB::open("/tmp/should_read_count_extend_units.db").unwrap(),
miner_db: MinerDB::open(":memory:").unwrap(),
temporarily_excluded_txids: HashSet::new(),
permanently_excluded_txids: HashSet::new(),
};
Expand Down
4 changes: 2 additions & 2 deletions stacks-node/src/tests/mem_abort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ fn run_with_memory_limits(
analysis_mem_limit: Option<u64>,
eval_mem_limit: Option<u64>,
) -> Result<Option<clarity::vm::Value>, ClarityError> {
let analysis_budget = ResourceBudget::new().with_max_memory_use(analysis_mem_limit);
let eval_budget = ResourceBudget::new().with_max_memory_use(eval_mem_limit);
let analysis_budget = ResourceBudget::unlimited().with_max_memory_use(analysis_mem_limit);
let eval_budget = ResourceBudget::unlimited().with_max_memory_use(eval_mem_limit);

let contract_id = QualifiedContractIdentifier::transient();
let mut marf = MemoryBackingStore::new();
Expand Down
19 changes: 11 additions & 8 deletions stackslib/src/chainstate/nakamoto/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::chainstate::nakamoto::{
};
use crate::chainstate::stacks::address::StacksAddressExtensions;
use crate::chainstate::stacks::db::blocks::{DummyEventDispatcher, MAX_RECEIPT_SIZES};
use crate::chainstate::stacks::db::transactions::TransactionProcessor;
use crate::chainstate::stacks::db::{
ChainstateTx, ClarityTx, StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo,
};
Expand Down Expand Up @@ -876,12 +877,13 @@ impl BlockBuilder for NakamotoBlockBuilder {
}

let cost_before = clarity_tx.cost_so_far();
let (_fee, receipt) = match StacksChainState::process_transaction_with_check(
clarity_tx,
tx,
quiet,
resource_budgets,
|receipt| {

let tx_processor = TransactionProcessor::from(tx)
.execute()
.using_clarity_tx(clarity_tx)
.with_resource_policy(*resource_budgets)
.quiet(quiet)
.with_check(|receipt| {
if !receipt.post_condition_aborted {
let all_events_valid = receipt.events.iter().all(|event| {
crate::net::api::postblock_proposal::is_event_pox_addr_valid(
Expand All @@ -905,8 +907,9 @@ impl BlockBuilder for NakamotoBlockBuilder {
*total_receipts_size = next_size;
Ok(())
}
},
) {
});

let (_fee, receipt) = match tx_processor.process() {
Ok(x) => x,
Err(e) => {
return parse_process_transaction_error(
Expand Down
70 changes: 4 additions & 66 deletions stackslib/src/chainstate/nakamoto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use super::stacks::boot::{
RewardSet, RewardSetData, BOOT_TEST_POX_4_AGG_KEY_CONTRACT, BOOT_TEST_POX_4_AGG_KEY_FNAME,
};
use super::stacks::db::accounts::MinerReward;
use super::stacks::db::transactions::TxToProcess;
use super::stacks::db::{
ChainstateTx, ClarityTx, MinerPaymentSchedule, MinerRewardInfo, StacksBlockHeaderTypes,
StacksEpochReceipt, StacksHeaderInfo,
Expand Down Expand Up @@ -651,9 +652,9 @@ pub struct SetupBlockResult<'a, 'b> {
/// is known-problematic and that its payload must not be executed during replay.
///
/// When a marker is present, replay still performs the static precheck, debits
/// the fee, and bumps the origin (and sponsor) nonces — but skips
/// `process_transaction_payload`. The `category` byte is opaque to consensus
/// and conveys the reason the miner/signers flagged the transaction.
/// the fee, and bumps the origin (and sponsor) nonces — but skips payload
/// processing. The `category` byte is opaque to consensus and conveys the
/// reason the miner/signers flagged the transaction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProblematicTxMarker {
/// Index into `NakamotoBlock::txs` of the problematic transaction.
Expand Down Expand Up @@ -681,69 +682,6 @@ impl StacksMessageCodec for ProblematicTxMarker {
/// block header. This bounds the header serialization.
pub const MAX_PROBLEMATIC_TX_MARKERS: usize = (MAX_BLOCK_LEN / MIN_TRANSACTION_LEN) as usize;

/// A transaction paired with its problematic marker.
///
/// Block replay consumes a list of these rather than a bare
/// `&[StacksTransaction]` plus a separate `problematic_txs` marker list. Fusing
/// the two up front (see [`NakamotoBlock::txs`]) means the replay
/// loop cannot execute a problematic transaction by forgetting to cross-
/// reference the markers: the disposition travels with the transaction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxToProcess<'a> {
/// Execute the transaction's payload normally.
Execute(&'a StacksTransaction),
/// Skip executing the transaction's payload because it was marked
/// problematic; the fee is still charged and nonces still bumped. Carries
/// the `category` byte from the marker.
Skip {
tx: &'a StacksTransaction,
category: u8,
},
}

impl<'a> TxToProcess<'a> {
/// Wrap a plain transaction list as all-to-execute, for blocks that carry
/// no problematic markers (every pre-Nakamoto block, and any Nakamoto block
/// with an empty marker list).
pub fn all_execute(txs: &'a [StacksTransaction]) -> impl Iterator<Item = TxToProcess<'a>> + 'a {
txs.iter().map(TxToProcess::Execute)
}

/// Get the transaction ID of this transaction, regardless of whether it's
/// marked problematic or not.
pub fn txid(&self) -> Txid {
match self {
TxToProcess::Execute(tx) | TxToProcess::Skip { tx, .. } => tx.txid(),
}
}

/// Get the raw transaction, regardless of whether it's marked problematic
/// and should not be executed.
pub fn payload(&self) -> &'a TransactionPayload {
match self {
TxToProcess::Execute(tx) | TxToProcess::Skip { tx, .. } => &tx.payload,
}
}

/// Is this transaction marked problematic?
pub fn is_problematic(&self) -> bool {
matches!(self, TxToProcess::Skip { .. })
}

/// Extract the underlying transaction, **deliberately ignoring** its
/// problematic state (whether it should be executed or skipped as
/// problematic).
///
/// This is the one escape hatch out of [`TxToProcess`]. Call it only when
/// the raw transaction is genuinely all that is needed, never on the
/// replay execution path, where the problematic state must be honored.
pub fn tx_ignoring_problematic_state(&self) -> &'a StacksTransaction {
match self {
TxToProcess::Execute(tx) | TxToProcess::Skip { tx, .. } => tx,
}
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NakamotoBlockHeader {
pub version: u8,
Expand Down
Loading