Skip to content

Commit e97ef7c

Browse files
committed
feat: add validation, test for signer validation replay blocks
1 parent a58ac52 commit e97ef7c

File tree

3 files changed

+528
-53
lines changed

3 files changed

+528
-53
lines changed

stackslib/src/core/test_util.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,17 +269,29 @@ pub fn to_addr(sk: &StacksPrivateKey) -> StacksAddress {
269269
StacksAddress::p2pkh(false, &StacksPublicKey::from_private(sk))
270270
}
271271

272-
pub fn make_stacks_transfer(
272+
pub fn make_stacks_transfer_tx(
273273
sender: &StacksPrivateKey,
274274
nonce: u64,
275275
tx_fee: u64,
276276
chain_id: u32,
277277
recipient: &PrincipalData,
278278
amount: u64,
279-
) -> Vec<u8> {
279+
) -> StacksTransaction {
280280
let payload =
281281
TransactionPayload::TokenTransfer(recipient.clone(), amount, TokenTransferMemo([0; 34]));
282-
let tx = sign_standard_single_sig_tx(payload, sender, nonce, tx_fee, chain_id);
282+
sign_standard_single_sig_tx(payload, sender, nonce, tx_fee, chain_id)
283+
}
284+
285+
/// Make a stacks transfer transaction, returning the serialized transaction bytes
286+
pub fn make_stacks_transfer(
287+
sender: &StacksPrivateKey,
288+
nonce: u64,
289+
tx_fee: u64,
290+
chain_id: u32,
291+
recipient: &PrincipalData,
292+
amount: u64,
293+
) -> Vec<u8> {
294+
let tx = make_stacks_transfer_tx(sender, nonce, tx_fee, chain_id, recipient, amount);
283295
let mut tx_bytes = vec![];
284296
tx.consensus_serialize(&mut tx_bytes).unwrap();
285297
tx_bytes

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder;
4848
use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState, NAKAMOTO_BLOCK_VERSION};
4949
use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
5050
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState};
51-
use crate::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction, TransactionResult};
51+
use crate::chainstate::stacks::miner::{
52+
BlockBuilder, BlockLimitFunction, TransactionError, TransactionResult,
53+
};
5254
use crate::chainstate::stacks::{
5355
Error as ChainError, StacksBlock, StacksBlockHeader, StacksTransaction, TransactionPayload,
5456
};
@@ -76,6 +78,11 @@ pub static TEST_VALIDATE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFla
7678
/// Artificial delay to add to block validation.
7779
pub static TEST_VALIDATE_DELAY_DURATION_SECS: LazyLock<TestFlag<u64>> =
7880
LazyLock::new(TestFlag::default);
81+
#[cfg(any(test, feature = "testing"))]
82+
/// Mock for the set of transactions that must be replayed
83+
pub static TEST_REPLAY_TRANSACTIONS: LazyLock<
84+
TestFlag<std::collections::VecDeque<StacksTransaction>>,
85+
> = LazyLock::new(TestFlag::default);
7986

8087
// This enum is used to supply a `reason_code` for validation
8188
// rejection responses. This is serialized as an enum with string
@@ -87,7 +94,8 @@ define_u8_enum![ValidateRejectCode {
8794
ChainstateError = 3,
8895
UnknownParent = 4,
8996
NonCanonicalTenure = 5,
90-
NoSuchTenure = 6
97+
NoSuchTenure = 6,
98+
InvalidTransactionReplay = 7
9199
}];
92100

93101
pub static TOO_MANY_REQUESTS_STATUS: u16 = 429;
@@ -372,6 +380,11 @@ impl NakamotoBlockProposal {
372380
/// - Miner signature is valid
373381
/// - Validation of transactions by executing them agains current chainstate.
374382
/// This is resource intensive, and therefore done only if previous checks pass
383+
///
384+
/// During transaction replay, we also check that the block only contains the unmined
385+
/// transactions that need to be replayed, up until either:
386+
/// - The set of transactions that must be replayed is exhausted
387+
/// - A cost limit is hit
375388
pub fn validate(
376389
&self,
377390
sortdb: &SortitionDB,
@@ -541,8 +554,70 @@ impl NakamotoBlockProposal {
541554
builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?;
542555
let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?;
543556

557+
// TODO: get replay set from stackerdb
558+
#[cfg(any(test, feature = "testing"))]
559+
let mut replay_txs_maybe = TEST_REPLAY_TRANSACTIONS.0.lock().unwrap().clone();
560+
#[cfg(not(any(test, feature = "testing")))]
561+
let mut replay_txs_maybe = None;
562+
544563
for (i, tx) in self.block.txs.iter().enumerate() {
545564
let tx_len = tx.tx_len();
565+
566+
// Check if tx is in the replay set, if required
567+
if let Some(ref mut replay_txs) = replay_txs_maybe {
568+
loop {
569+
let Some(replay_tx) = replay_txs.pop_front() else {
570+
return Err(BlockValidateRejectReason {
571+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
572+
reason: "Transaction is not in the replay set".into(),
573+
});
574+
};
575+
if replay_tx.txid() == tx.txid() {
576+
break;
577+
}
578+
let tx_result = builder.try_mine_tx_with_len(
579+
&mut tenure_tx,
580+
&replay_tx,
581+
replay_tx.tx_len(),
582+
&BlockLimitFunction::NO_LIMIT_HIT,
583+
ASTRules::PrecheckSize,
584+
None,
585+
);
586+
match tx_result {
587+
TransactionResult::ProcessingError(e) => {
588+
// The tx wasn't able to be mined. Check `TransactionError`, to
589+
// see if we should error or allow the tx to be dropped from the replay set.
590+
591+
// TODO: handle TransactionError cases
592+
match e.error {
593+
ChainError::BlockCostExceeded => {
594+
// block limit reached; add tx back to replay set.
595+
// BUT we know that the block should have ended at this point, so
596+
// return an error.
597+
replay_txs.push_front(replay_tx);
598+
599+
return Err(BlockValidateRejectReason {
600+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
601+
reason: "Transaction is not in the replay set".into(),
602+
});
603+
}
604+
_ => {
605+
// it's ok, drop it
606+
continue;
607+
}
608+
}
609+
}
610+
_ => {
611+
// Tx should have been included
612+
return Err(BlockValidateRejectReason {
613+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
614+
reason: "Transaction is not in the replay set".into(),
615+
});
616+
}
617+
};
618+
}
619+
}
620+
546621
let tx_result = builder.try_mine_tx_with_len(
547622
&mut tenure_tx,
548623
tx,

0 commit comments

Comments
 (0)