Skip to content

Commit 1058df1

Browse files
committed
fix: handling of block cost exceeded in validation
1 parent 86185f2 commit 1058df1

File tree

3 files changed

+132
-10
lines changed

3 files changed

+132
-10
lines changed

stackslib/src/core/test_util.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use clarity::codec::StacksMessageCodec;
55
use clarity::types::chainstate::{
66
BlockHeaderHash, ConsensusHash, StacksAddress, StacksPrivateKey, StacksPublicKey,
77
};
8+
use clarity::vm::costs::ExecutionCost;
89
use clarity::vm::tests::BurnStateDB;
910
use clarity::vm::types::PrincipalData;
1011
use clarity::vm::{ClarityName, ClarityVersion, ContractName, Value};
@@ -524,3 +525,25 @@ pub fn insert_tx_in_mempool(
524525
.execute(sql, args)
525526
.expect("Failed to insert transaction into mempool");
526527
}
528+
529+
/// Generate source code for a contract that exposes a public function
530+
/// `big-tx`. This function uses `proportion` of read_count when called
531+
pub fn make_big_read_count_contract(limit: ExecutionCost, proportion: u64) -> String {
532+
let read_count = (limit.read_count * proportion) / 100;
533+
534+
let read_lines = (0..read_count)
535+
.map(|_| format!("(var-get my-var)"))
536+
.collect::<Vec<_>>()
537+
.join("\n");
538+
539+
format!(
540+
"
541+
(define-data-var my-var uint u0)
542+
(define-public (big-tx)
543+
(begin
544+
{}
545+
(ok true)))
546+
",
547+
read_lines
548+
)
549+
}

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState, NAKAMOTO_BL
5050
use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
5151
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState};
5252
use crate::chainstate::stacks::miner::{
53-
BlockBuilder, BlockLimitFunction, TransactionError, TransactionResult,
53+
BlockBuilder, BlockLimitFunction, TransactionError, TransactionProblematic, TransactionResult,
54+
TransactionSkipped,
5455
};
5556
use crate::chainstate::stacks::{
5657
Error as ChainError, StacksBlock, StacksBlockHeader, StacksTransaction, TransactionPayload,
@@ -579,6 +580,10 @@ impl NakamotoBlockProposal {
579580
if replay_tx.txid() == tx.txid() {
580581
break;
581582
}
583+
584+
// The included tx doesn't match the next tx in the
585+
// replay set. Check to see if the tx is skipped because
586+
// it was unmineable.
582587
let tx_result = builder.try_mine_tx_with_len(
583588
&mut tenure_tx,
584589
&replay_tx,
@@ -588,30 +593,42 @@ impl NakamotoBlockProposal {
588593
None,
589594
);
590595
match tx_result {
591-
TransactionResult::ProcessingError(e) => {
592-
// The tx wasn't able to be mined. Check `TransactionError`, to
593-
// see if we should error or allow the tx to be dropped from the replay set.
594-
595-
// TODO: handle TransactionError cases
596-
match e.error {
597-
ChainError::BlockCostExceeded => {
596+
TransactionResult::Skipped(TransactionSkipped { error, .. })
597+
| TransactionResult::ProcessingError(TransactionError { error, .. })
598+
| TransactionResult::Problematic(TransactionProblematic {
599+
error, ..
600+
}) => {
601+
// The tx wasn't able to be mined. Check the underlying error, to
602+
// see if we should reject the block or allow the tx to be
603+
// dropped from the replay set.
604+
605+
match error {
606+
ChainError::CostOverflowError(..)
607+
| ChainError::BlockTooBigError => {
598608
// block limit reached; add tx back to replay set.
599609
// BUT we know that the block should have ended at this point, so
600610
// return an error.
611+
let txid = replay_tx.txid();
601612
replay_txs.push_front(replay_tx);
602613

614+
warn!("Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block.";
615+
"error" => %error,
616+
"txid" => %txid,
617+
);
618+
603619
return Err(BlockValidateRejectReason {
604620
reason_code: ValidateRejectCode::InvalidTransactionReplay,
605621
reason: "Transaction is not in the replay set".into(),
606622
});
607623
}
624+
// TODO: handle other ChainError cases
608625
_ => {
609626
// it's ok, drop it
610627
continue;
611628
}
612629
}
613630
}
614-
_ => {
631+
TransactionResult::Success(_) => {
615632
// Tx should have been included
616633
return Err(BlockValidateRejectReason {
617634
reason_code: ValidateRejectCode::InvalidTransactionReplay,

stackslib/src/net/api/tests/postblock_proposal.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ use crate::chainstate::nakamoto::NakamotoChainState;
3434
use crate::chainstate::stacks::db::StacksChainState;
3535
use crate::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction};
3636
use crate::chainstate::stacks::test::make_codec_test_nakamoto_block;
37-
use crate::core::test_util::{make_stacks_transfer_tx, to_addr};
37+
use crate::core::test_util::{
38+
make_big_read_count_contract, make_contract_call, make_contract_publish,
39+
make_stacks_transfer_tx, to_addr,
40+
};
41+
use crate::core::BLOCK_LIMIT_MAINNET_21;
3842
use crate::net::api::postblock_proposal::{
3943
BlockValidateOk, BlockValidateReject, TEST_REPLAY_TRANSACTIONS,
4044
};
@@ -867,3 +871,81 @@ fn replay_validation_test_transaction_mineable_mismatch_series_2() {
867871
}
868872
}
869873
}
874+
875+
#[test]
876+
#[ignore]
877+
/// Replay set has [deploy, big_a, big_b, c]
878+
/// The block has [deploy, big_a, c]
879+
///
880+
/// The block should have ended at big_a, because big_b would
881+
/// have cost too much to include.
882+
fn replay_validation_test_budget_exceeded() {
883+
let result = replay_validation_test(|rpc_test| {
884+
let miner_privk = &rpc_test.peer_1.miner.nakamoto_miner_key();
885+
let miner_addr = to_addr(miner_privk);
886+
887+
let contract_code = make_big_read_count_contract(BLOCK_LIMIT_MAINNET_21, 50);
888+
889+
let deploy_tx_bytes = make_contract_publish(
890+
miner_privk,
891+
36,
892+
1000,
893+
CHAIN_ID_TESTNET,
894+
&"big-contract",
895+
&contract_code,
896+
);
897+
898+
let big_a_bytes = make_contract_call(
899+
miner_privk,
900+
37,
901+
1000,
902+
CHAIN_ID_TESTNET,
903+
&miner_addr,
904+
&"big-contract",
905+
"big-tx",
906+
&vec![],
907+
);
908+
909+
let big_b_bytes = make_contract_call(
910+
miner_privk,
911+
38,
912+
1000,
913+
CHAIN_ID_TESTNET,
914+
&miner_addr,
915+
&"big-contract",
916+
"big-tx",
917+
&vec![],
918+
);
919+
920+
let deploy_tx =
921+
StacksTransaction::consensus_deserialize(&mut deploy_tx_bytes.as_slice()).unwrap();
922+
let big_a = StacksTransaction::consensus_deserialize(&mut big_a_bytes.as_slice()).unwrap();
923+
let big_b = StacksTransaction::consensus_deserialize(&mut big_b_bytes.as_slice()).unwrap();
924+
925+
let transfer_tx = make_stacks_transfer_tx(
926+
miner_privk,
927+
38,
928+
1000,
929+
CHAIN_ID_TESTNET,
930+
&StandardPrincipalData::transient().into(),
931+
100,
932+
);
933+
934+
(
935+
vec![deploy_tx.clone(), big_a.clone(), big_b.clone()].into(),
936+
vec![deploy_tx, big_a, transfer_tx],
937+
)
938+
});
939+
940+
match result {
941+
Ok(_) => {
942+
panic!("Expected validation to be rejected, but got Ok");
943+
}
944+
Err(rejection) => {
945+
assert_eq!(
946+
rejection.reason_code,
947+
ValidateRejectCode::InvalidTransactionReplay
948+
);
949+
}
950+
}
951+
}

0 commit comments

Comments
 (0)