Skip to content

Commit d0edcdb

Browse files
committed
feat: use global signer state machine replay set
1 parent 5e4cd7a commit d0edcdb

File tree

5 files changed

+264
-9
lines changed

5 files changed

+264
-9
lines changed

libsigner/src/events.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ use blockstack_lib::chainstate::stacks::StacksTransaction;
2929
use blockstack_lib::net::api::postblock_proposal::{
3030
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
3131
};
32-
use blockstack_lib::net::api::{prefix_hex, prefix_opt_hex};
3332
use blockstack_lib::net::stackerdb::MINER_SLOT_COUNT;
3433
use blockstack_lib::util_lib::boot::boot_code_id;
3534
use blockstack_lib::version_string;
@@ -46,7 +45,8 @@ pub use stacks_common::consts::SIGNER_SLOTS_PER_USER;
4645
use stacks_common::types::chainstate::{
4746
BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksPublicKey,
4847
};
49-
use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
48+
use stacks_common::util::hash::{hex_bytes, Hash160, Sha512Trunc256Sum};
49+
use stacks_common::util::serde_serializers::{prefix_hex, prefix_opt_hex, prefix_string_0x};
5050
use stacks_common::util::HexError;
5151
use stacks_common::versions::STACKS_NODE_VERSION;
5252
use tiny_http::{
@@ -229,6 +229,8 @@ pub enum SignerEvent<T: SignerEventTrait> {
229229
signer_sighash: Option<Sha512Trunc256Sum>,
230230
/// The block height for the newly processed stacks block
231231
block_height: u64,
232+
/// The transactions included in the block
233+
transactions: Vec<StacksTransaction>,
232234
},
233235
}
234236

@@ -613,6 +615,45 @@ impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
613615
}
614616
}
615617

618+
/// A subset of `TransactionEventPayload`, received from the event
619+
/// dispatcher.
620+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
621+
pub struct NewBlockTransaction {
622+
/// The raw transaction bytes. If this is a burn operation,
623+
/// this will be "00".
624+
#[serde(with = "prefix_string_0x")]
625+
raw_tx: String,
626+
}
627+
628+
impl NewBlockTransaction {
629+
pub fn get_stacks_transaction(&self) -> Result<Option<StacksTransaction>, CodecError> {
630+
if self.raw_tx == "00" {
631+
Ok(None)
632+
} else {
633+
let tx_bytes = hex_bytes(&self.raw_tx).map_err(|e| {
634+
CodecError::DeserializeError(format!("Failed to deserialize raw tx: {}", e))
635+
})?;
636+
let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..])?;
637+
Ok(Some(tx))
638+
}
639+
}
640+
}
641+
642+
/// "Special" deserializer to turn `{ tx_raw: "0x..." }` into `StacksTransaction`.
643+
fn deserialize_raw_tx_hex<'de, D: serde::Deserializer<'de>>(
644+
d: D,
645+
) -> Result<Vec<StacksTransaction>, D::Error> {
646+
let tx_objs: Vec<NewBlockTransaction> = serde::Deserialize::deserialize(d)?;
647+
Ok(tx_objs
648+
.iter()
649+
.map(|tx| tx.get_stacks_transaction())
650+
.collect::<Result<Vec<_>, _>>()
651+
.map_err(serde::de::Error::custom)?
652+
.into_iter()
653+
.flatten()
654+
.collect::<Vec<_>>())
655+
}
656+
616657
#[derive(Debug, Deserialize)]
617658
struct BlockEvent {
618659
#[serde(with = "prefix_hex")]
@@ -625,6 +666,8 @@ struct BlockEvent {
625666
#[serde(with = "prefix_hex")]
626667
block_hash: BlockHeaderHash,
627668
block_height: u64,
669+
#[serde(deserialize_with = "deserialize_raw_tx_hex")]
670+
transactions: Vec<StacksTransaction>,
628671
}
629672

630673
impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
@@ -636,6 +679,7 @@ impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
636679
block_id: block_event.index_block_hash,
637680
consensus_hash: block_event.consensus_hash,
638681
block_height: block_event.block_height,
682+
transactions: block_event.transactions,
639683
})
640684
}
641685
}

libsigner/src/v0/signer_state.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,13 @@ impl GlobalStateEvaluator {
182182
pub fn reached_agreement(&self, vote_weight: u32) -> bool {
183183
vote_weight >= self.total_weight * 7 / 10
184184
}
185+
186+
/// Get the global transaction replay set. Returns `None` if there
187+
/// is no global state.
188+
pub fn get_global_tx_replay_set(&mut self) -> Option<ReplayTransactionSet> {
189+
let global_state = self.determine_global_state()?;
190+
Some(global_state.tx_replay_set)
191+
}
185192
}
186193

187194
/// A "wrapper" struct around Vec<StacksTransaction> that behaves like

stacks-signer/src/v0/signer.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ impl Signer {
525525
block_id,
526526
consensus_hash,
527527
signer_sighash,
528+
transactions,
528529
} => {
529530
let Some(signer_sighash) = signer_sighash else {
530531
debug!("{self}: received a new block event for a pre-nakamoto block, no processing necessary");
@@ -536,10 +537,11 @@ impl Signer {
536537
"block_id" => %block_id,
537538
"signer_signature_hash" => %signer_sighash,
538539
"consensus_hash" => %consensus_hash,
539-
"block_height" => block_height
540+
"block_height" => block_height,
541+
"total_txs" => transactions.len()
540542
);
541543
self.local_state_machine
542-
.stacks_block_arrival(consensus_hash, *block_height, block_id, signer_sighash, &self.signer_db)
544+
.stacks_block_arrival(consensus_hash, *block_height, block_id, signer_sighash, &self.signer_db, transactions)
543545
.unwrap_or_else(|e| error!("{self}: failed to update local state machine for latest stacks block arrival"; "err" => ?e));
544546

545547
if let Ok(Some(mut block_info)) = self
@@ -1615,7 +1617,10 @@ impl Signer {
16151617
if is_block_found || !self.validate_with_replay_tx {
16161618
None
16171619
} else {
1618-
self.local_state_machine.get_tx_replay_set()
1620+
self.global_state_evaluator
1621+
.get_global_tx_replay_set()
1622+
.unwrap_or_default()
1623+
.into_optional()
16191624
},
16201625
) {
16211626
Ok(_) => {

stacks-signer/src/v0/signer_state.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

1616
use std::collections::HashMap;
17+
#[cfg(any(test, feature = "testing"))]
18+
use std::sync::LazyLock;
1719
use std::time::{Duration, UNIX_EPOCH};
1820

1921
use blockstack_lib::chainstate::burn::ConsensusHashExtensions;
2022
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
21-
use blockstack_lib::chainstate::stacks::{StacksTransaction, TransactionPayload};
23+
use blockstack_lib::chainstate::stacks::{
24+
StacksTransaction, TenureChangeCause, TenureChangePayload, TransactionPayload,
25+
};
2226
use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal;
2327
use clarity::types::chainstate::StacksAddress;
28+
#[cfg(any(test, feature = "testing"))]
29+
use clarity::util::tests::TestFlag;
2430
use libsigner::v0::messages::{
2531
MessageSlotID, SignerMessage, StateMachineUpdate as StateMachineUpdateMessage,
2632
StateMachineUpdateContent, StateMachineUpdateMinerState,
@@ -34,6 +40,8 @@ use stacks_common::codec::Error as CodecError;
3440
use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash};
3541
use stacks_common::util::hash::Sha512Trunc256Sum;
3642
use stacks_common::util::secp256k1::MessageSignature;
43+
#[cfg(any(test, feature = "testing"))]
44+
use stacks_common::util::secp256k1::Secp256k1PublicKey;
3745
use stacks_common::{debug, info, warn};
3846

3947
use crate::chainstate::{
@@ -45,6 +53,11 @@ use crate::signerdb::SignerDb;
4553
/// This is the latest supported protocol version for this signer binary
4654
pub static SUPPORTED_SIGNER_PROTOCOL_VERSION: u64 = 1;
4755

56+
/// Vec of pubkeys that should ignore checking for a bitcoin fork
57+
#[cfg(any(test, feature = "testing"))]
58+
pub static TEST_IGNORE_BITCOIN_FORK_PUBKEYS: LazyLock<TestFlag<Vec<Secp256k1PublicKey>>> =
59+
LazyLock::new(TestFlag::default);
60+
4861
/// The local signer state machine
4962
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5063
pub enum LocalStateMachine {
@@ -346,6 +359,7 @@ impl LocalStateMachine {
346359
block_id: &StacksBlockId,
347360
signer_signature_hash: &Sha512Trunc256Sum,
348361
db: &SignerDb,
362+
txs: &Vec<StacksTransaction>,
349363
) -> Result<(), SignerChainstateError> {
350364
// set self to uninitialized so that if this function errors,
351365
// self is left as uninitialized.
@@ -383,7 +397,25 @@ impl LocalStateMachine {
383397
);
384398
prior_state_machine.tx_replay_set = ReplayTransactionSet::none();
385399
}
386-
Ok(false) => {}
400+
Ok(false) => {
401+
info!("Signer state: got a new block during replay that wasn't validated by our replay set.";
402+
"txs" => ?txs,
403+
);
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+
}
418+
}
387419
Err(e) => {
388420
warn!("Failed to check if block was validated by replay tx";
389421
"err" => ?e,
@@ -861,6 +893,17 @@ impl LocalStateMachine {
861893
"prior_state_machine.burn_block_height" => prior_state_machine.burn_block_height,
862894
"prior_state_machine.burn_block" => %prior_state_machine.burn_block,
863895
);
896+
#[cfg(any(test, feature = "testing"))]
897+
{
898+
let ignore_bitcoin_fork = TEST_IGNORE_BITCOIN_FORK_PUBKEYS
899+
.get()
900+
.iter()
901+
.any(|pubkey| &StacksAddress::p2pkh(false, pubkey) == client.get_signer_address());
902+
if ignore_bitcoin_fork {
903+
warn!("Ignoring bitcoin fork due to test flag");
904+
return Ok(None);
905+
}
906+
}
864907
// Determine the tenures that were forked
865908
let mut parent_burn_block_info =
866909
db.get_burn_block_by_ch(&prior_state_machine.burn_block)?;

0 commit comments

Comments
 (0)