Skip to content

Commit 90014e5

Browse files
authored
Merge pull request #6109 from hstove/feat/full-tx-replay
feat: validate blocks against replay set
2 parents b39dfbc + 9cf8ffd commit 90014e5

File tree

20 files changed

+1081
-295
lines changed

20 files changed

+1081
-295
lines changed

libsigner/src/events.rs

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ pub use stacks_common::consts::SIGNER_SLOTS_PER_USER;
4545
use stacks_common::types::chainstate::{
4646
BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksPublicKey,
4747
};
48-
use stacks_common::util::hash::{Hash160, Sha512Trunc256Sum};
49-
use stacks_common::util::serde_serializers::{prefix_hex, prefix_opt_hex};
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

@@ -577,18 +579,26 @@ impl<T: SignerEventTrait> TryFrom<BlockValidateResponse> for SignerEvent<T> {
577579
}
578580
}
579581

580-
#[derive(Debug, Deserialize)]
581-
struct BurnBlockEvent {
582+
/// Burn block JSON payload from the event receiver
583+
#[derive(Debug, Deserialize, Clone)]
584+
pub struct BurnBlockEvent {
585+
/// The hash of the burn block
582586
#[serde(with = "prefix_hex")]
583-
burn_block_hash: BurnchainHeaderHash,
584-
burn_block_height: u64,
585-
reward_recipients: Vec<serde_json::Value>,
586-
reward_slot_holders: Vec<String>,
587-
burn_amount: u64,
587+
pub burn_block_hash: BurnchainHeaderHash,
588+
/// The height of the burn block
589+
pub burn_block_height: u64,
590+
/// The reward recipients
591+
pub reward_recipients: Vec<serde_json::Value>,
592+
/// The reward slot holders
593+
pub reward_slot_holders: Vec<String>,
594+
/// The amount of burn
595+
pub burn_amount: u64,
596+
/// The consensus hash of the burn block
588597
#[serde(with = "prefix_hex")]
589-
consensus_hash: ConsensusHash,
598+
pub consensus_hash: ConsensusHash,
599+
/// The parent burn block hash
590600
#[serde(with = "prefix_hex")]
591-
parent_burn_block_hash: BurnchainHeaderHash,
601+
pub parent_burn_block_hash: BurnchainHeaderHash,
592602
}
593603

594604
impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
@@ -605,6 +615,45 @@ impl<T: SignerEventTrait> TryFrom<BurnBlockEvent> for SignerEvent<T> {
605615
}
606616
}
607617

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+
608657
#[derive(Debug, Deserialize)]
609658
struct BlockEvent {
610659
#[serde(with = "prefix_hex")]
@@ -617,6 +666,8 @@ struct BlockEvent {
617666
#[serde(with = "prefix_hex")]
618667
block_hash: BlockHeaderHash,
619668
block_height: u64,
669+
#[serde(deserialize_with = "deserialize_raw_tx_hex")]
670+
transactions: Vec<StacksTransaction>,
620671
}
621672

622673
impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
@@ -628,6 +679,7 @@ impl<T: SignerEventTrait> TryFrom<BlockEvent> for SignerEvent<T> {
628679
block_id: block_event.index_block_hash,
629680
consensus_hash: block_event.consensus_hash,
630681
block_height: block_event.block_height,
682+
transactions: block_event.transactions,
631683
})
632684
}
633685
}

libsigner/src/libsigner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ use stacks_common::versions::STACKS_SIGNER_VERSION;
5757

5858
pub use crate::error::{EventError, RPCError};
5959
pub use crate::events::{
60-
BlockProposal, BlockProposalData, EventReceiver, EventStopSignaler, SignerEvent,
61-
SignerEventReceiver, SignerEventTrait, SignerStopSignaler,
60+
BlockProposal, BlockProposalData, BurnBlockEvent, EventReceiver, EventStopSignaler,
61+
SignerEvent, SignerEventReceiver, SignerEventTrait, SignerStopSignaler,
6262
};
6363
pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop};
6464
pub use crate::session::{SignerSession, StackerDBSession};

libsigner/src/tests/signer_state.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::v0::messages::{
2424
StateMachineUpdate as StateMachineUpdateMessage, StateMachineUpdateContent,
2525
StateMachineUpdateMinerState,
2626
};
27-
use crate::v0::signer_state::{GlobalStateEvaluator, SignerStateMachine};
27+
use crate::v0::signer_state::{GlobalStateEvaluator, ReplayTransactionSet, SignerStateMachine};
2828

2929
fn generate_global_state_evaluator(num_addresses: u32) -> GlobalStateEvaluator {
3030
let address_weights = generate_random_address_with_equal_weights(num_addresses);
@@ -237,7 +237,7 @@ fn determine_global_states() {
237237
burn_block_height,
238238
current_miner: (&current_miner).into(),
239239
active_signer_protocol_version: local_supported_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
240-
tx_replay_set: None,
240+
tx_replay_set: ReplayTransactionSet::none(),
241241
};
242242

243243
global_eval.insert_update(local_address, local_update);
@@ -276,7 +276,7 @@ fn determine_global_states() {
276276
burn_block_height,
277277
current_miner: (&new_miner).into(),
278278
active_signer_protocol_version: local_supported_signer_protocol_version, // a majority of signers are saying they support version the same local_supported_signer_protocol_version, so update it here...
279-
tx_replay_set: None,
279+
tx_replay_set: ReplayTransactionSet::none(),
280280
};
281281

282282
global_eval.insert_update(local_address, new_update);

libsigner/src/v0/signer_state.rs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,12 @@ impl GlobalStateEvaluator {
132132
burn_block_height,
133133
current_miner,
134134
..
135-
} => (burn_block, burn_block_height, current_miner, None),
135+
} => (
136+
burn_block,
137+
burn_block_height,
138+
current_miner,
139+
ReplayTransactionSet::none(),
140+
),
136141
StateMachineUpdateContent::V1 {
137142
burn_block,
138143
burn_block_height,
@@ -142,7 +147,7 @@ impl GlobalStateEvaluator {
142147
burn_block,
143148
burn_block_height,
144149
current_miner,
145-
Some(replay_transactions.clone()),
150+
ReplayTransactionSet::new(replay_transactions.clone()),
146151
),
147152
};
148153
let state_machine = SignerStateMachine {
@@ -177,6 +182,77 @@ impl GlobalStateEvaluator {
177182
pub fn reached_agreement(&self, vote_weight: u32) -> bool {
178183
vote_weight >= self.total_weight * 7 / 10
179184
}
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+
}
192+
}
193+
194+
/// A "wrapper" struct around Vec<StacksTransaction> that behaves like
195+
/// `None` when the vector is empty.
196+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash)]
197+
pub struct ReplayTransactionSet(Vec<StacksTransaction>);
198+
199+
impl ReplayTransactionSet {
200+
/// Create a new `ReplayTransactionSet`
201+
pub fn new(tx_replay_set: Vec<StacksTransaction>) -> Self {
202+
Self(tx_replay_set)
203+
}
204+
205+
/// Check if the `ReplayTransactionSet` is empty
206+
pub fn is_empty(&self) -> bool {
207+
self.0.is_empty()
208+
}
209+
210+
/// Map into an optional, returning `None` if the set is empty
211+
pub fn clone_as_optional(&self) -> Option<Vec<StacksTransaction>> {
212+
if self.is_empty() {
213+
None
214+
} else {
215+
Some(self.0.clone())
216+
}
217+
}
218+
219+
/// Unwrap the `ReplayTransactionSet` or return a default vector if it is empty
220+
pub fn unwrap_or_default(self) -> Vec<StacksTransaction> {
221+
if self.is_empty() {
222+
vec![]
223+
} else {
224+
self.0
225+
}
226+
}
227+
228+
/// Map the transactions in the set to a new type, only
229+
/// if the set is not empty
230+
pub fn map<U, F>(self, f: F) -> Option<U>
231+
where
232+
F: Fn(Vec<StacksTransaction>) -> U,
233+
{
234+
if self.is_empty() {
235+
None
236+
} else {
237+
Some(f(self.0))
238+
}
239+
}
240+
241+
/// Create a new `ReplayTransactionSet` with no transactions
242+
pub fn none() -> Self {
243+
Self(vec![])
244+
}
245+
246+
/// Check if the `ReplayTransactionSet` isn't empty
247+
pub fn is_some(&self) -> bool {
248+
!self.is_empty()
249+
}
250+
}
251+
252+
impl Default for ReplayTransactionSet {
253+
fn default() -> Self {
254+
Self::none()
255+
}
180256
}
181257

182258
/// A signer state machine view. This struct can
@@ -193,7 +269,7 @@ pub struct SignerStateMachine {
193269
/// The active signing protocol version
194270
pub active_signer_protocol_version: u64,
195271
/// Transaction replay set
196-
pub tx_replay_set: Option<Vec<StacksTransaction>>,
272+
pub tx_replay_set: ReplayTransactionSet,
197273
}
198274

199275
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash)]

stacks-signer/src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ pub(crate) mod tests {
431431
block_proposal_max_age_secs: config.block_proposal_max_age_secs,
432432
reorg_attempts_activity_timeout: config.reorg_attempts_activity_timeout,
433433
proposal_wait_for_parent_time: config.proposal_wait_for_parent_time,
434+
validate_with_replay_tx: config.validate_with_replay_tx,
434435
}
435436
}
436437

stacks-signer/src/client/stacks_client.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use std::collections::{HashMap, VecDeque};
1818
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
1919
use blockstack_lib::chainstate::stacks::boot::{NakamotoSignerEntry, SIGNERS_NAME};
2020
use blockstack_lib::chainstate::stacks::db::StacksBlockHeaderTypes;
21-
use blockstack_lib::chainstate::stacks::TransactionVersion;
21+
use blockstack_lib::chainstate::stacks::{StacksTransaction, TransactionVersion};
2222
use blockstack_lib::net::api::callreadonly::CallReadOnlyResponse;
2323
use blockstack_lib::net::api::get_tenures_fork_info::{
2424
TenureForkingInfo, RPC_TENURE_FORKING_INFO_PATH,
@@ -302,7 +302,11 @@ impl StacksClient {
302302
}
303303

304304
/// Submit the block proposal to the stacks node. The block will be validated and returned via the HTTP endpoint for Block events.
305-
pub fn submit_block_for_validation(&self, block: NakamotoBlock) -> Result<(), ClientError> {
305+
pub fn submit_block_for_validation(
306+
&self,
307+
block: NakamotoBlock,
308+
replay_txs: Option<Vec<StacksTransaction>>,
309+
) -> Result<(), ClientError> {
306310
debug!("StacksClient: Submitting block for validation";
307311
"signer_signature_hash" => %block.header.signer_signature_hash(),
308312
"block_id" => %block.header.block_id(),
@@ -311,7 +315,7 @@ impl StacksClient {
311315
let block_proposal = NakamotoBlockProposal {
312316
block,
313317
chain_id: self.chain_id,
314-
replay_txs: None,
318+
replay_txs,
315319
};
316320
let timer = crate::monitoring::actions::new_rpc_call_timer(
317321
&self.block_proposal_path(),
@@ -1094,7 +1098,7 @@ mod tests {
10941098
header,
10951099
txs: vec![],
10961100
};
1097-
let h = spawn(move || mock.client.submit_block_for_validation(block));
1101+
let h = spawn(move || mock.client.submit_block_for_validation(block, None));
10981102
write_response(mock.server, b"HTTP/1.1 200 OK\n\n");
10991103
assert!(h.join().unwrap().is_ok());
11001104
}
@@ -1107,7 +1111,7 @@ mod tests {
11071111
header,
11081112
txs: vec![],
11091113
};
1110-
let h = spawn(move || mock.client.submit_block_for_validation(block));
1114+
let h = spawn(move || mock.client.submit_block_for_validation(block, None));
11111115
write_response(mock.server, b"HTTP/1.1 404 Not Found\n\n");
11121116
assert!(h.join().unwrap().is_err());
11131117
}

0 commit comments

Comments
 (0)