Skip to content

Commit 8c45d50

Browse files
committed
fix: logic around whether to clear replay set
1 parent 8b605b4 commit 8c45d50

File tree

5 files changed

+100
-59
lines changed

5 files changed

+100
-59
lines changed

stacks-signer/src/signerdb.rs

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ static ADD_BLOCK_VALIDATED_BY_REPLAY_TXS_TABLE: &str = r#"
605605
CREATE TABLE IF NOT EXISTS block_validated_by_replay_txs (
606606
signer_signature_hash TEXT NOT NULL,
607607
replay_tx_hash TEXT NOT NULL,
608+
replay_tx_exhausted INTEGER NOT NULL,
608609
PRIMARY KEY (signer_signature_hash, replay_tx_hash)
609610
) STRICT;"#;
610611

@@ -1549,10 +1550,15 @@ impl SignerDb {
15491550
&self,
15501551
signer_signature_hash: &Sha512Trunc256Sum,
15511552
replay_tx_hash: u64,
1553+
replay_tx_exhausted: bool,
15521554
) -> Result<(), DBError> {
15531555
self.db.execute(
1554-
"INSERT INTO block_validated_by_replay_txs (signer_signature_hash, replay_tx_hash) VALUES (?1, ?2)",
1555-
params![signer_signature_hash.to_string(), format!("{replay_tx_hash}")],
1556+
"INSERT INTO block_validated_by_replay_txs (signer_signature_hash, replay_tx_hash, replay_tx_exhausted) VALUES (?1, ?2, ?3)",
1557+
params![
1558+
signer_signature_hash.to_string(),
1559+
format!("{replay_tx_hash}"),
1560+
replay_tx_exhausted
1561+
],
15561562
)?;
15571563
Ok(())
15581564
}
@@ -1562,14 +1568,13 @@ impl SignerDb {
15621568
&self,
15631569
signer_signature_hash: &Sha512Trunc256Sum,
15641570
replay_tx_hash: u64,
1565-
) -> Result<bool, DBError> {
1566-
let query = "SELECT replay_tx_hash FROM block_validated_by_replay_txs WHERE signer_signature_hash = ? AND replay_tx_hash = ?";
1571+
) -> Result<Option<BlockValidatedByReplaySet>, DBError> {
1572+
let query = "SELECT replay_tx_hash, replay_tx_exhausted FROM block_validated_by_replay_txs WHERE signer_signature_hash = ? AND replay_tx_hash = ?";
15671573
let args = params![
15681574
signer_signature_hash.to_string(),
15691575
format!("{replay_tx_hash}")
15701576
];
1571-
let replay_tx_hash_opt: Option<String> = query_row(&self.db, query, args)?;
1572-
Ok(replay_tx_hash_opt.is_some())
1577+
query_row(&self.db, query, args)
15731578
}
15741579
}
15751580

@@ -1604,6 +1609,25 @@ impl FromRow<PendingBlockValidation> for PendingBlockValidation {
16041609
}
16051610
}
16061611

1612+
/// A struct used to represent whether a block was validated by a transaction replay set
1613+
pub struct BlockValidatedByReplaySet {
1614+
/// The hash of the transaction replay set that validated the block
1615+
pub replay_tx_hash: String,
1616+
/// Whether the transaction replay set exhausted the set of transactions
1617+
pub replay_tx_exhausted: bool,
1618+
}
1619+
1620+
impl FromRow<BlockValidatedByReplaySet> for BlockValidatedByReplaySet {
1621+
fn from_row(row: &rusqlite::Row) -> Result<Self, DBError> {
1622+
let replay_tx_hash = row.get_unwrap(0);
1623+
let replay_tx_exhausted = row.get_unwrap(1);
1624+
Ok(BlockValidatedByReplaySet {
1625+
replay_tx_hash,
1626+
replay_tx_exhausted,
1627+
})
1628+
}
1629+
}
1630+
16071631
#[cfg(any(test, feature = "testing"))]
16081632
impl SignerDb {
16091633
/// For tests, fetch all pending block validations
@@ -2851,13 +2875,37 @@ pub mod tests {
28512875

28522876
let signer_signature_hash = Sha512Trunc256Sum([0; 32]);
28532877
let replay_tx_hash = 15559610262907183370_u64;
2878+
let replay_tx_exhausted = true;
2879+
2880+
db.insert_block_validated_by_replay_tx(
2881+
&signer_signature_hash,
2882+
replay_tx_hash,
2883+
replay_tx_exhausted,
2884+
)
2885+
.expect("Failed to insert block validated by replay tx");
28542886

2855-
db.insert_block_validated_by_replay_tx(&signer_signature_hash, replay_tx_hash)
2856-
.expect("Failed to insert block validated by replay tx");
2887+
let result = db
2888+
.get_was_block_validated_by_replay_tx(&signer_signature_hash, replay_tx_hash)
2889+
.expect("Failed to get block validated by replay tx")
2890+
.expect("Expected block validation result to be stored");
2891+
assert_eq!(result.replay_tx_hash, format!("{replay_tx_hash}"));
2892+
assert!(result.replay_tx_exhausted);
2893+
2894+
let replay_tx_hash = 15559610262907183369_u64;
2895+
let replay_tx_exhausted = false;
2896+
2897+
db.insert_block_validated_by_replay_tx(
2898+
&signer_signature_hash,
2899+
replay_tx_hash,
2900+
replay_tx_exhausted,
2901+
)
2902+
.expect("Failed to insert block validated by replay tx");
28572903

28582904
let result = db
28592905
.get_was_block_validated_by_replay_tx(&signer_signature_hash, replay_tx_hash)
2860-
.expect("Failed to get block validated by replay tx");
2861-
assert!(result);
2906+
.expect("Failed to get block validated by replay tx")
2907+
.expect("Expected block validation result to be stored");
2908+
assert_eq!(result.replay_tx_hash, format!("{replay_tx_hash}"));
2909+
assert!(!result.replay_tx_exhausted);
28622910
}
28632911
}

stacks-signer/src/v0/signer.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use std::sync::LazyLock;
2020
use std::time::{Duration, Instant, SystemTime};
2121

2222
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
23-
use blockstack_lib::chainstate::stacks::TransactionPayload;
2423
use blockstack_lib::net::api::postblock_proposal::{
2524
BlockValidateOk, BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
2625
TOO_MANY_REQUESTS_STATUS,
@@ -1043,7 +1042,11 @@ impl Signer {
10431042
"replay_tx_hash" => replay_tx_hash
10441043
);
10451044
self.signer_db
1046-
.insert_block_validated_by_replay_tx(&signer_signature_hash, replay_tx_hash)
1045+
.insert_block_validated_by_replay_tx(
1046+
&signer_signature_hash,
1047+
replay_tx_hash,
1048+
block_validate_ok.replay_tx_exhausted,
1049+
)
10471050
.unwrap_or_else(|e| {
10481051
warn!("{self}: Failed to insert block validated by replay tx: {e:?}")
10491052
});
@@ -1614,21 +1617,15 @@ impl Signer {
16141617
debug!("{self}: Cannot confirm that we have processed parent, but we've waited proposal_wait_for_parent_time, will submit proposal");
16151618
}
16161619
}
1617-
let is_block_found = block.txs.iter().all(|tx| {
1618-
matches!(
1619-
tx.payload,
1620-
TransactionPayload::Coinbase(..) | TransactionPayload::TenureChange(..)
1621-
)
1622-
});
16231620
match stacks_client.submit_block_for_validation(
16241621
block.clone(),
1625-
if is_block_found || !self.validate_with_replay_tx {
1626-
None
1627-
} else {
1622+
if self.validate_with_replay_tx {
16281623
self.global_state_evaluator
16291624
.get_global_tx_replay_set()
16301625
.unwrap_or_default()
16311626
.into_optional()
1627+
} else {
1628+
None
16321629
},
16331630
) {
16341631
Ok(_) => {

stacks-signer/src/v0/signer_state.rs

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ use std::time::{Duration, UNIX_EPOCH};
2020

2121
use blockstack_lib::chainstate::burn::ConsensusHashExtensions;
2222
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
23-
use blockstack_lib::chainstate::stacks::{
24-
StacksTransaction, TenureChangeCause, TenureChangePayload, TransactionPayload,
25-
};
23+
use blockstack_lib::chainstate::stacks::{StacksTransaction, TransactionPayload};
2624
use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal;
2725
use clarity::types::chainstate::StacksAddress;
2826
#[cfg(any(test, feature = "testing"))]
@@ -48,7 +46,7 @@ use crate::chainstate::{
4846
ProposalEvalConfig, SignerChainstateError, SortitionState, SortitionsView,
4947
};
5048
use crate::client::{ClientError, CurrentAndLastSortition, StackerDB, StacksClient};
51-
use crate::signerdb::SignerDb;
49+
use crate::signerdb::{BlockValidatedByReplaySet, SignerDb};
5250

5351
/// This is the latest supported protocol version for this signer binary
5452
pub static SUPPORTED_SIGNER_PROTOCOL_VERSION: u64 = 1;
@@ -389,32 +387,24 @@ impl LocalStateMachine {
389387
&prior_state_machine.tx_replay_set.into_optional(),
390388
) {
391389
match db.get_was_block_validated_by_replay_tx(signer_signature_hash, replay_set_hash) {
392-
Ok(true) => {
393-
// This block was validated by our current state machine's replay set,
394-
// and the block exhausted the replay set. Therefore, clear the tx replay set.
395-
info!("Signer State: Incoming Stacks block exhausted the replay set, clearing the tx replay set";
396-
"signer_signature_hash" => %signer_signature_hash,
397-
);
398-
prior_state_machine.tx_replay_set = ReplayTransactionSet::none();
390+
Ok(Some(BlockValidatedByReplaySet {
391+
replay_tx_exhausted,
392+
..
393+
})) => {
394+
if replay_tx_exhausted {
395+
// This block was validated by our current state machine's replay set,
396+
// and the block exhausted the replay set. Therefore, clear the tx replay set.
397+
info!("Signer State: Incoming Stacks block exhausted the replay set, clearing the tx replay set";
398+
"signer_signature_hash" => %signer_signature_hash,
399+
);
400+
prior_state_machine.tx_replay_set = ReplayTransactionSet::none();
401+
}
399402
}
400-
Ok(false) => {
401-
info!("Signer state: got a new block during replay that wasn't validated by our replay set.";
403+
Ok(None) => {
404+
info!("Signer state: got a new block during replay that wasn't validated by our replay set. Clearing the local replay set.";
402405
"txs" => ?txs,
403406
);
404-
// If any of the transactions aren't TenureChange/Coinbase, we should
405-
// capitulate and clear the tx replay set.
406-
if txs.iter().any(|tx| {
407-
!matches!(
408-
tx.payload,
409-
TransactionPayload::TenureChange(TenureChangePayload {
410-
cause: TenureChangeCause::BlockFound,
411-
..
412-
}) | TransactionPayload::Coinbase(..)
413-
)
414-
}) {
415-
info!("Signer state: clearing the tx replay set due to unrecognized transactions");
416-
prior_state_machine.tx_replay_set = ReplayTransactionSet::none();
417-
}
407+
prior_state_machine.tx_replay_set = ReplayTransactionSet::none();
418408
}
419409
Err(e) => {
420410
warn!("Failed to check if block was validated by replay tx";

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,11 @@ pub struct BlockValidateOk {
176176
pub size: u64,
177177
pub validation_time_ms: u64,
178178
/// If a block was validated by a transaction replay set,
179-
/// and the block exhausted the set of transactions,
180179
/// then this return Some with the hash of the replay set.
181180
pub replay_tx_hash: Option<u64>,
181+
/// If a block was validated by a transaction replay set,
182+
/// then this is true if this block exhausted the set of transactions.
183+
pub replay_tx_exhausted: bool,
182184
}
183185

184186
/// This enum is used for serializing the response to block
@@ -582,10 +584,7 @@ impl NakamotoBlockProposal {
582584
loop {
583585
if matches!(
584586
tx.payload,
585-
TransactionPayload::TenureChange(TenureChangePayload {
586-
cause: TenureChangeCause::Extended,
587-
..
588-
})
587+
TransactionPayload::TenureChange(..) | TransactionPayload::Coinbase(..)
589588
) {
590589
// Allow this to happen, tenure extend checks happen elsewhere.
591590
break;
@@ -759,16 +758,15 @@ impl NakamotoBlockProposal {
759758
})
760759
);
761760

761+
let replay_tx_hash = Self::tx_replay_hash(&self.replay_txs);
762+
762763
Ok(BlockValidateOk {
763764
signer_signature_hash: block.header.signer_signature_hash(),
764765
cost,
765766
size,
766767
validation_time_ms,
767-
replay_tx_hash: if replay_tx_exhausted {
768-
Self::tx_replay_hash(&self.replay_txs)
769-
} else {
770-
None
771-
},
768+
replay_tx_hash,
769+
replay_tx_exhausted,
772770
})
773771
}
774772

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ fn test_try_make_response() {
442442
size,
443443
validation_time_ms,
444444
replay_tx_hash,
445+
replay_tx_exhausted,
445446
}) => {
446447
assert_eq!(
447448
signer_signature_hash,
@@ -451,6 +452,7 @@ fn test_try_make_response() {
451452
assert_eq!(size, 180);
452453
assert!(validation_time_ms > 0 && validation_time_ms < 60000);
453454
assert!(replay_tx_hash.is_none());
455+
assert!(!replay_tx_exhausted);
454456
}
455457
_ => panic!("expected ok"),
456458
}
@@ -745,6 +747,7 @@ fn replay_validation_test_transaction_unmineable_match_2() {
745747
let replay_hash = hasher.finish();
746748

747749
assert_eq!(block_validate_ok.replay_tx_hash, Some(replay_hash));
750+
assert!(block_validate_ok.replay_tx_exhausted);
748751
}
749752
Err(rejection) => {
750753
panic!("Expected validation to be OK, but got {:?}", rejection);
@@ -1032,7 +1035,12 @@ fn replay_validation_test_budget_exhausted() {
10321035

10331036
match result {
10341037
Ok(block_validate_ok) => {
1035-
assert_eq!(block_validate_ok.replay_tx_hash, None);
1038+
let mut hasher = DefaultHasher::new();
1039+
replay_set.hash(&mut hasher);
1040+
let replay_hash = hasher.finish();
1041+
1042+
assert_eq!(block_validate_ok.replay_tx_hash, Some(replay_hash));
1043+
assert!(!block_validate_ok.replay_tx_exhausted);
10361044
}
10371045
Err(rejection) => {
10381046
panic!(

0 commit comments

Comments
 (0)