Skip to content

Commit 6ed6cfe

Browse files
committed
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into fix/reconsider-invalid-parent-block-proposals
2 parents 9a19b37 + e505550 commit 6ed6cfe

File tree

15 files changed

+851
-202
lines changed

15 files changed

+851
-202
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
99

1010
### Changed
1111

12+
- Added new `ValidateRejectCode` values to the `/v3/block_proposal` endpoint
1213
- Reduce the default `block_rejection_timeout_steps` configuration so that miners will retry faster when blocks fail to reach 70% approved or 30% rejected.
1314

1415
## [3.1.0.0.8]

stacks-signer/src/client/stacks_client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ impl StacksClient {
313313
let block_proposal = NakamotoBlockProposal {
314314
block,
315315
chain_id: self.chain_id,
316+
replay_txs: None,
316317
};
317318
let timer = crate::monitoring::actions::new_rpc_call_timer(
318319
&self.block_proposal_path(),

stackslib/src/core/test_util.rs

Lines changed: 38 additions & 3 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};
@@ -269,17 +270,29 @@ pub fn to_addr(sk: &StacksPrivateKey) -> StacksAddress {
269270
StacksAddress::p2pkh(false, &StacksPublicKey::from_private(sk))
270271
}
271272

272-
pub fn make_stacks_transfer(
273+
pub fn make_stacks_transfer_tx(
273274
sender: &StacksPrivateKey,
274275
nonce: u64,
275276
tx_fee: u64,
276277
chain_id: u32,
277278
recipient: &PrincipalData,
278279
amount: u64,
279-
) -> Vec<u8> {
280+
) -> StacksTransaction {
280281
let payload =
281282
TransactionPayload::TokenTransfer(recipient.clone(), amount, TokenTransferMemo([0; 34]));
282-
let tx = sign_standard_single_sig_tx(payload, sender, nonce, tx_fee, chain_id);
283+
sign_standard_single_sig_tx(payload, sender, nonce, tx_fee, chain_id)
284+
}
285+
286+
/// Make a stacks transfer transaction, returning the serialized transaction bytes
287+
pub fn make_stacks_transfer_serialized(
288+
sender: &StacksPrivateKey,
289+
nonce: u64,
290+
tx_fee: u64,
291+
chain_id: u32,
292+
recipient: &PrincipalData,
293+
amount: u64,
294+
) -> Vec<u8> {
295+
let tx = make_stacks_transfer_tx(sender, nonce, tx_fee, chain_id, recipient, amount);
283296
let mut tx_bytes = vec![];
284297
tx.consensus_serialize(&mut tx_bytes).unwrap();
285298
tx_bytes
@@ -512,3 +525,25 @@ pub fn insert_tx_in_mempool(
512525
.execute(sql, args)
513526
.expect("Failed to insert transaction into mempool");
514527
}
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/core/tests/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ use crate::core::mempool::{
6565
db_get_all_nonces, MemPoolSyncData, MemPoolWalkSettings, MemPoolWalkTxTypes, TxTag,
6666
BLOOM_COUNTER_DEPTH, BLOOM_COUNTER_ERROR_RATE, MAX_BLOOM_COUNTER_TXS,
6767
};
68-
use crate::core::test_util::{insert_tx_in_mempool, make_stacks_transfer, to_addr};
68+
use crate::core::test_util::{insert_tx_in_mempool, make_stacks_transfer_serialized, to_addr};
6969
use crate::core::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH};
7070
use crate::net::Error as NetError;
7171
use crate::util_lib::bloom::test::setup_bloom_counter;
@@ -2795,7 +2795,7 @@ fn large_mempool() {
27952795
let sender_addr = to_addr(sender_sk);
27962796
let fee = thread_rng().gen_range(180..2000);
27972797
let transfer_tx =
2798-
make_stacks_transfer(sender_sk, *nonce, fee, 0x80000000, &recipient, 1);
2798+
make_stacks_transfer_serialized(sender_sk, *nonce, fee, 0x80000000, &recipient, 1);
27992799
insert_tx_in_mempool(
28002800
&mempool_tx,
28012801
transfer_tx,

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
use std::collections::VecDeque;
1718
use std::io::{Read, Write};
1819
#[cfg(any(test, feature = "testing"))]
1920
use std::sync::LazyLock;
@@ -48,10 +49,14 @@ use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder;
4849
use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState, NAKAMOTO_BLOCK_VERSION};
4950
use crate::chainstate::stacks::db::blocks::MINIMUM_TX_FEE_RATE_PER_BYTE;
5051
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState};
51-
use crate::chainstate::stacks::miner::{BlockBuilder, BlockLimitFunction, TransactionResult};
52+
use crate::chainstate::stacks::miner::{
53+
BlockBuilder, BlockLimitFunction, TransactionError, TransactionProblematic, TransactionResult,
54+
TransactionSkipped,
55+
};
5256
use crate::chainstate::stacks::{
5357
Error as ChainError, StacksBlock, StacksBlockHeader, StacksTransaction, TransactionPayload,
5458
};
59+
use crate::clarity_vm::clarity::Error as ClarityError;
5560
use crate::core::mempool::{MemPoolDB, ProposalCallbackReceiver};
5661
use crate::cost_estimates::FeeRateEstimate;
5762
use crate::net::http::{
@@ -76,6 +81,11 @@ pub static TEST_VALIDATE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFla
7681
/// Artificial delay to add to block validation.
7782
pub static TEST_VALIDATE_DELAY_DURATION_SECS: LazyLock<TestFlag<u64>> =
7883
LazyLock::new(TestFlag::default);
84+
#[cfg(any(test, feature = "testing"))]
85+
/// Mock for the set of transactions that must be replayed
86+
pub static TEST_REPLAY_TRANSACTIONS: LazyLock<
87+
TestFlag<std::collections::VecDeque<StacksTransaction>>,
88+
> = LazyLock::new(TestFlag::default);
7989

8090
// This enum is used to supply a `reason_code` for validation
8191
// rejection responses. This is serialized as an enum with string
@@ -88,9 +98,10 @@ define_u8_enum![ValidateRejectCode {
8898
UnknownParent = 4,
8999
NonCanonicalTenure = 5,
90100
NoSuchTenure = 6,
91-
InvalidParentBlock = 7,
92-
InvalidTimestamp = 8,
93-
NetworkChainMismatch = 9
101+
InvalidTransactionReplay = 7,
102+
InvalidParentBlock = 8,
103+
InvalidTimestamp = 9,
104+
NetworkChainMismatch = 10
94105
}];
95106

96107
pub static TOO_MANY_REQUESTS_STATUS: u16 = 429;
@@ -210,6 +221,8 @@ pub struct NakamotoBlockProposal {
210221
pub block: NakamotoBlock,
211222
/// Identifies which chain block is for (Mainnet, Testnet, etc.)
212223
pub chain_id: u32,
224+
/// Optional transaction replay set
225+
pub replay_txs: Option<Vec<StacksTransaction>>,
213226
}
214227

215228
impl NakamotoBlockProposal {
@@ -375,6 +388,11 @@ impl NakamotoBlockProposal {
375388
/// - Miner signature is valid
376389
/// - Validation of transactions by executing them agains current chainstate.
377390
/// This is resource intensive, and therefore done only if previous checks pass
391+
///
392+
/// During transaction replay, we also check that the block only contains the unmined
393+
/// transactions that need to be replayed, up until either:
394+
/// - The set of transactions that must be replayed is exhausted
395+
/// - A cost limit is hit
378396
pub fn validate(
379397
&self,
380398
sortdb: &SortitionDB,
@@ -544,8 +562,88 @@ impl NakamotoBlockProposal {
544562
builder.load_tenure_info(chainstate, &burn_dbconn, tenure_cause)?;
545563
let mut tenure_tx = builder.tenure_begin(&burn_dbconn, &mut miner_tenure_info)?;
546564

565+
let mut replay_txs_maybe: Option<VecDeque<StacksTransaction>> =
566+
self.replay_txs.clone().map(|txs| txs.into());
567+
547568
for (i, tx) in self.block.txs.iter().enumerate() {
548569
let tx_len = tx.tx_len();
570+
571+
// If a list of replay transactions is set, this transaction must be the next
572+
// mineable transaction from this list.
573+
if let Some(ref mut replay_txs) = replay_txs_maybe {
574+
loop {
575+
let Some(replay_tx) = replay_txs.pop_front() else {
576+
// During transaction replay, we expect that the block only
577+
// contains transactions from the replay set. Thus, if we're here,
578+
// the block contains a transaction that is not in the replay set,
579+
// and we should reject the block.
580+
return Err(BlockValidateRejectReason {
581+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
582+
reason: "Transaction is not in the replay set".into(),
583+
});
584+
};
585+
if replay_tx.txid() == tx.txid() {
586+
break;
587+
}
588+
589+
// The included tx doesn't match the next tx in the
590+
// replay set. Check to see if the tx is skipped because
591+
// it was unmineable.
592+
let tx_result = builder.try_mine_tx_with_len(
593+
&mut tenure_tx,
594+
&replay_tx,
595+
replay_tx.tx_len(),
596+
&BlockLimitFunction::NO_LIMIT_HIT,
597+
ASTRules::PrecheckSize,
598+
None,
599+
);
600+
match tx_result {
601+
TransactionResult::Skipped(TransactionSkipped { error, .. })
602+
| TransactionResult::ProcessingError(TransactionError { error, .. })
603+
| TransactionResult::Problematic(TransactionProblematic {
604+
error, ..
605+
}) => {
606+
// The tx wasn't able to be mined. Check the underlying error, to
607+
// see if we should reject the block or allow the tx to be
608+
// dropped from the replay set.
609+
610+
match error {
611+
ChainError::CostOverflowError(..)
612+
| ChainError::BlockTooBigError
613+
| ChainError::ClarityError(ClarityError::CostError(..)) => {
614+
// block limit reached; add tx back to replay set.
615+
// BUT we know that the block should have ended at this point, so
616+
// return an error.
617+
let txid = replay_tx.txid();
618+
replay_txs.push_front(replay_tx);
619+
620+
warn!("Rejecting block proposal. Next replay tx exceeds cost limits, so should have been in the next block.";
621+
"error" => %error,
622+
"txid" => %txid,
623+
);
624+
625+
return Err(BlockValidateRejectReason {
626+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
627+
reason: "Transaction is not in the replay set".into(),
628+
});
629+
}
630+
_ => {
631+
// it's ok, drop it
632+
continue;
633+
}
634+
}
635+
}
636+
TransactionResult::Success(_) => {
637+
// Tx should have been included
638+
return Err(BlockValidateRejectReason {
639+
reason_code: ValidateRejectCode::InvalidTransactionReplay,
640+
reason: "Transaction is not in the replay set".into(),
641+
});
642+
}
643+
};
644+
}
645+
}
646+
549647
let tx_result = builder.try_mine_tx_with_len(
550648
&mut tenure_tx,
551649
tx,

0 commit comments

Comments
 (0)