diff --git a/libsigner/src/v0/messages.rs b/libsigner/src/v0/messages.rs index 7d411f89b5..5f7b82a937 100644 --- a/libsigner/src/v0/messages.rs +++ b/libsigner/src/v0/messages.rs @@ -77,9 +77,7 @@ define_u8_enum!( /// the contract index in the signers contracts (i.e., X in signers-0-X) MessageSlotID { /// Block Response message from signers - BlockResponse = 1, - /// Mock Signature message from Epoch 2.5 signers - MockSignature = 2 + BlockResponse = 1 }); define_u8_enum!( @@ -115,8 +113,12 @@ SignerMessageTypePrefix { BlockResponse = 1, /// Block Pushed message from miners BlockPushed = 2, - /// Mock Signature message from Epoch 2.5 signers - MockSignature = 3 + /// Mock block proposal message from Epoch 2.5 miners + MockProposal = 3, + /// Mock block signature message from Epoch 2.5 signers + MockSignature = 4, + /// Mock block message from Epoch 2.5 miners + MockBlock = 5 }); #[cfg_attr(test, mutants::skip)] @@ -159,7 +161,9 @@ impl From<&SignerMessage> for SignerMessageTypePrefix { SignerMessage::BlockProposal(_) => SignerMessageTypePrefix::BlockProposal, SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse, SignerMessage::BlockPushed(_) => SignerMessageTypePrefix::BlockPushed, + SignerMessage::MockProposal(_) => SignerMessageTypePrefix::MockProposal, SignerMessage::MockSignature(_) => SignerMessageTypePrefix::MockSignature, + SignerMessage::MockBlock(_) => SignerMessageTypePrefix::MockBlock, } } } @@ -175,6 +179,10 @@ pub enum SignerMessage { BlockPushed(NakamotoBlock), /// A mock signature from the epoch 2.5 signers MockSignature(MockSignature), + /// A mock message from the epoch 2.5 miners + MockProposal(MockProposal), + /// A mock block from the epoch 2.5 miners + MockBlock(MockBlock), } impl SignerMessage { @@ -184,9 +192,11 @@ impl SignerMessage { #[cfg_attr(test, mutants::skip)] pub fn msg_id(&self) -> Option { match self { - Self::BlockProposal(_) | Self::BlockPushed(_) => None, - Self::BlockResponse(_) => Some(MessageSlotID::BlockResponse), - Self::MockSignature(_) => Some(MessageSlotID::MockSignature), + Self::BlockProposal(_) + | Self::BlockPushed(_) + | Self::MockProposal(_) + | Self::MockBlock(_) => None, + Self::BlockResponse(_) | Self::MockSignature(_) => Some(MessageSlotID::BlockResponse), // Mock signature uses the same slot as block response since its exclusively for epoch 2.5 testing } } } @@ -201,6 +211,8 @@ impl StacksMessageCodec for SignerMessage { SignerMessage::BlockResponse(block_response) => block_response.consensus_serialize(fd), SignerMessage::BlockPushed(block) => block.consensus_serialize(fd), SignerMessage::MockSignature(signature) => signature.consensus_serialize(fd), + SignerMessage::MockProposal(message) => message.consensus_serialize(fd), + SignerMessage::MockBlock(block) => block.consensus_serialize(fd), }?; Ok(()) } @@ -222,10 +234,18 @@ impl StacksMessageCodec for SignerMessage { let block = StacksMessageCodec::consensus_deserialize(fd)?; SignerMessage::BlockPushed(block) } + SignerMessageTypePrefix::MockProposal => { + let message = StacksMessageCodec::consensus_deserialize(fd)?; + SignerMessage::MockProposal(message) + } SignerMessageTypePrefix::MockSignature => { let signature = StacksMessageCodec::consensus_deserialize(fd)?; SignerMessage::MockSignature(signature) } + SignerMessageTypePrefix::MockBlock => { + let block = StacksMessageCodec::consensus_deserialize(fd)?; + SignerMessage::MockBlock(block) + } }; Ok(message) } @@ -295,110 +315,75 @@ impl StacksMessageCodec for PeerInfo { } } -/// A snapshot of the signer view of the stacks node to be used for mock signing. +/// A mock block proposal for Epoch 2.5 mock signing #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct MockSignData { - /// The view of the stacks node peer information at the time of the mock signature +pub struct MockProposal { + /// The view of the stacks node peer information at the time of the mock proposal pub peer_info: PeerInfo, - /// The burn block height of the event that triggered the mock signature - pub event_burn_block_height: u64, - /// The chain id for the mock signature + /// The chain id for the mock proposal pub chain_id: u32, -} - -impl StacksMessageCodec for MockSignData { - fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { - self.peer_info.consensus_serialize(fd)?; - write_next(fd, &self.event_burn_block_height)?; - write_next(fd, &self.chain_id)?; - Ok(()) - } - - fn consensus_deserialize(fd: &mut R) -> Result { - let peer_info = PeerInfo::consensus_deserialize(fd)?; - let event_burn_block_height = read_next::(fd)?; - let chain_id = read_next::(fd)?; - Ok(Self { - peer_info, - event_burn_block_height, - chain_id, - }) - } -} - -/// A mock signature for the stacks node to be used for mock signing. -/// This is only used by Epoch 2.5 signers to simulate the signing of a block for every sortition. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct MockSignature { - /// The signature of the mock signature + /// The miner's signature across the peer info signature: MessageSignature, - /// The data that was signed across - pub sign_data: MockSignData, } -impl MockSignature { - /// Create a new mock sign data struct from the provided event burn block height, peer info, chain id, and private key. - /// Note that peer burn block height and event burn block height may not be the same if the peer view is stale. - pub fn new( - event_burn_block_height: u64, - peer_info: PeerInfo, - chain_id: u32, - stacks_private_key: &StacksPrivateKey, - ) -> Self { +impl MockProposal { + /// Create a new mock proposal data struct from the provided peer info, chain id, and private key. + pub fn new(peer_info: PeerInfo, chain_id: u32, stacks_private_key: &StacksPrivateKey) -> Self { let mut sig = Self { signature: MessageSignature::empty(), - sign_data: MockSignData { - peer_info, - event_burn_block_height, - chain_id, - }, + chain_id, + peer_info, }; sig.sign(stacks_private_key) - .expect("Failed to sign MockSignature"); + .expect("Failed to sign MockProposal"); sig } - /// The signature hash for the mock signature - pub fn signature_hash(&self) -> Sha256Sum { - let domain_tuple = - make_structured_data_domain("mock-signer", "1.0.0", self.sign_data.chain_id); + /// The signature hash for the mock proposal + pub fn miner_signature_hash(&self) -> Sha256Sum { + let domain_tuple = make_structured_data_domain("mock-miner", "1.0.0", self.chain_id); let data_tuple = Value::Tuple( TupleData::from_data(vec![ ( "stacks-tip-consensus-hash".into(), - Value::buff_from( - self.sign_data - .peer_info - .stacks_tip_consensus_hash - .as_bytes() - .into(), - ) - .unwrap(), + Value::buff_from(self.peer_info.stacks_tip_consensus_hash.as_bytes().into()) + .unwrap(), ), ( "stacks-tip".into(), - Value::buff_from(self.sign_data.peer_info.stacks_tip.as_bytes().into()) - .unwrap(), + Value::buff_from(self.peer_info.stacks_tip.as_bytes().into()).unwrap(), ), ( "stacks-tip-height".into(), - Value::UInt(self.sign_data.peer_info.stacks_tip_height.into()), + Value::UInt(self.peer_info.stacks_tip_height.into()), ), ( "server-version".into(), - Value::string_ascii_from_bytes( - self.sign_data.peer_info.server_version.clone().into(), - ) - .unwrap(), + Value::string_ascii_from_bytes(self.peer_info.server_version.clone().into()) + .unwrap(), + ), + ( + "pox-consensus".into(), + Value::buff_from(self.peer_info.pox_consensus.as_bytes().into()).unwrap(), ), + ]) + .expect("Error creating signature hash"), + ); + structured_data_message_hash(data_tuple, domain_tuple) + } + + /// The signature hash including the miner's signature. Used by signers. + fn signer_signature_hash(&self) -> Sha256Sum { + let domain_tuple = make_structured_data_domain("mock-signer", "1.0.0", self.chain_id); + let data_tuple = Value::Tuple( + TupleData::from_data(vec![ ( - "event-burn-block-height".into(), - Value::UInt(self.sign_data.event_burn_block_height.into()), + "miner-signature-hash".into(), + Value::buff_from(self.miner_signature_hash().as_bytes().into()).unwrap(), ), ( - "pox-consensus".into(), - Value::buff_from(self.sign_data.peer_info.pox_consensus.as_bytes().into()) - .unwrap(), + "miner-signature".into(), + Value::buff_from(self.signature.as_bytes().into()).unwrap(), ), ]) .expect("Error creating signature hash"), @@ -406,18 +391,79 @@ impl MockSignature { structured_data_message_hash(data_tuple, domain_tuple) } + /// Sign the mock proposal and set the internal signature field + fn sign(&mut self, private_key: &StacksPrivateKey) -> Result<(), String> { + let signature_hash = self.miner_signature_hash(); + self.signature = private_key.sign(signature_hash.as_bytes())?; + Ok(()) + } + /// Verify the mock proposal against the provided miner public key + pub fn verify(&self, public_key: &StacksPublicKey) -> Result { + if self.signature == MessageSignature::empty() { + return Ok(false); + } + let signature_hash = self.miner_signature_hash(); + public_key + .verify(&signature_hash.0, &self.signature) + .map_err(|e| e.to_string()) + } +} + +impl StacksMessageCodec for MockProposal { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + self.peer_info.consensus_serialize(fd)?; + write_next(fd, &self.chain_id)?; + write_next(fd, &self.signature)?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let peer_info = PeerInfo::consensus_deserialize(fd)?; + let chain_id = read_next::(fd)?; + let signature = read_next::(fd)?; + Ok(Self { + peer_info, + chain_id, + signature, + }) + } +} + +/// A mock signature for the stacks node to be used for mock signing. +/// This is only used by Epoch 2.5 signers to simulate the signing of a block for every sortition. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MockSignature { + /// The signer's signature across the mock proposal + signature: MessageSignature, + /// The mock block proposal that was signed across + pub mock_proposal: MockProposal, +} + +impl MockSignature { + /// Create a new mock signature from the provided proposal and signer private key. + pub fn new(mock_proposal: MockProposal, stacks_private_key: &StacksPrivateKey) -> Self { + let mut sig = Self { + signature: MessageSignature::empty(), + mock_proposal, + }; + sig.sign(stacks_private_key) + .expect("Failed to sign MockSignature"); + sig + } + /// Sign the mock signature and set the internal signature field fn sign(&mut self, private_key: &StacksPrivateKey) -> Result<(), String> { - let signature_hash = self.signature_hash(); + let signature_hash = self.mock_proposal.signer_signature_hash(); self.signature = private_key.sign(signature_hash.as_bytes())?; Ok(()) } - /// Verify the mock signature against the provided public key + + /// Verify the mock signature against the provided signer public key pub fn verify(&self, public_key: &StacksPublicKey) -> Result { if self.signature == MessageSignature::empty() { return Ok(false); } - let signature_hash = self.signature_hash(); + let signature_hash = self.mock_proposal.signer_signature_hash(); public_key .verify(&signature_hash.0, &self.signature) .map_err(|e| e.to_string()) @@ -427,16 +473,42 @@ impl MockSignature { impl StacksMessageCodec for MockSignature { fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { write_next(fd, &self.signature)?; - self.sign_data.consensus_serialize(fd)?; + self.mock_proposal.consensus_serialize(fd)?; Ok(()) } fn consensus_deserialize(fd: &mut R) -> Result { let signature = read_next::(fd)?; - let sign_data = read_next::(fd)?; + let mock_proposal = MockProposal::consensus_deserialize(fd)?; Ok(Self { signature, - sign_data, + mock_proposal, + }) + } +} + +/// The mock block data for epoch 2.5 miners to broadcast to simulate block signing +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct MockBlock { + /// The mock proposal that was signed across + pub mock_proposal: MockProposal, + /// The mock signatures that the miner received + pub mock_signatures: Vec, +} + +impl StacksMessageCodec for MockBlock { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + self.mock_proposal.consensus_serialize(fd)?; + write_next(fd, &self.mock_signatures)?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let mock_proposal = MockProposal::consensus_deserialize(fd)?; + let mock_signatures = read_next::, _>(fd)?; + Ok(Self { + mock_proposal, + mock_signatures, }) } } @@ -739,6 +811,7 @@ mod test { use clarity::types::PrivateKey; use clarity::util::hash::MerkleTree; use clarity::util::secp256k1::MessageSignature; + use rand::rngs::mock; use rand::{thread_rng, Rng, RngCore}; use rand_core::OsRng; use stacks_common::bitvec::BitVec; @@ -868,7 +941,7 @@ mod test { pox_consensus: ConsensusHash([pox_consensus_byte; 20]), } } - fn random_mock_sign_data() -> MockSignData { + fn random_mock_proposal() -> MockProposal { let chain_byte: u8 = thread_rng().gen_range(0..=1); let chain_id = if chain_byte == 1 { CHAIN_ID_TESTNET @@ -876,39 +949,36 @@ mod test { CHAIN_ID_MAINNET }; let peer_info = random_peer_data(); - MockSignData { + MockProposal { peer_info, - event_burn_block_height: thread_rng().next_u64(), chain_id, + signature: MessageSignature::empty(), } } #[test] - fn verify_sign_mock_signature() { + fn verify_sign_mock_proposal() { let private_key = StacksPrivateKey::new(); let public_key = StacksPublicKey::from_private(&private_key); let bad_private_key = StacksPrivateKey::new(); let bad_public_key = StacksPublicKey::from_private(&bad_private_key); - let mut mock_signature = MockSignature { - signature: MessageSignature::empty(), - sign_data: random_mock_sign_data(), - }; - assert!(!mock_signature + let mut mock_proposal = random_mock_proposal(); + assert!(!mock_proposal .verify(&public_key) - .expect("Failed to verify MockSignature")); + .expect("Failed to verify MockProposal")); - mock_signature + mock_proposal .sign(&private_key) - .expect("Failed to sign MockSignature"); + .expect("Failed to sign MockProposal"); - assert!(mock_signature + assert!(mock_proposal .verify(&public_key) - .expect("Failed to verify MockSignature")); - assert!(!mock_signature + .expect("Failed to verify MockProposal")); + assert!(!mock_proposal .verify(&bad_public_key) - .expect("Failed to verify MockSignature")); + .expect("Failed to verify MockProposal")); } #[test] @@ -920,12 +990,25 @@ mod test { assert_eq!(peer_data, deserialized_data); } + #[test] + fn serde_mock_proposal() { + let mut mock_signature = random_mock_proposal(); + mock_signature.sign(&StacksPrivateKey::new()).unwrap(); + let serialized_signature = mock_signature.serialize_to_vec(); + let deserialized_signature = read_next::(&mut &serialized_signature[..]) + .expect("Failed to deserialize MockSignature"); + assert_eq!(mock_signature, deserialized_signature); + } + #[test] fn serde_mock_signature() { - let mock_signature = MockSignature { + let mut mock_signature = MockSignature { signature: MessageSignature::empty(), - sign_data: random_mock_sign_data(), + mock_proposal: random_mock_proposal(), }; + mock_signature + .sign(&StacksPrivateKey::new()) + .expect("Failed to sign MockSignature"); let serialized_signature = mock_signature.serialize_to_vec(); let deserialized_signature = read_next::(&mut &serialized_signature[..]) .expect("Failed to deserialize MockSignature"); @@ -933,11 +1016,17 @@ mod test { } #[test] - fn serde_sign_data() { - let sign_data = random_mock_sign_data(); - let serialized_data = sign_data.serialize_to_vec(); - let deserialized_data = read_next::(&mut &serialized_data[..]) + fn serde_mock_block() { + let mock_proposal = random_mock_proposal(); + let mock_signature_1 = MockSignature::new(mock_proposal.clone(), &StacksPrivateKey::new()); + let mock_signature_2 = MockSignature::new(mock_proposal.clone(), &StacksPrivateKey::new()); + let mock_block = MockBlock { + mock_proposal, + mock_signatures: vec![mock_signature_1, mock_signature_2], + }; + let serialized_data = mock_block.serialize_to_vec(); + let deserialized_data = read_next::(&mut &serialized_data[..]) .expect("Failed to deserialize MockSignData"); - assert_eq!(sign_data, deserialized_data); + assert_eq!(mock_block, deserialized_data); } } diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index e51d96d933..53a288b7f5 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -18,13 +18,12 @@ use std::sync::mpsc::Sender; use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; -use clarity::consts::{CHAIN_ID_MAINNET, CHAIN_ID_TESTNET}; use clarity::types::chainstate::StacksPrivateKey; use clarity::types::{PrivateKey, StacksEpochId}; use clarity::util::hash::MerkleHashFunc; use clarity::util::secp256k1::Secp256k1PublicKey; use libsigner::v0::messages::{ - BlockResponse, MessageSlotID, MockSignature, RejectCode, SignerMessage, + BlockResponse, MessageSlotID, MockProposal, MockSignature, RejectCode, SignerMessage, }; use libsigner::{BlockProposal, SignerEvent}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; @@ -159,6 +158,25 @@ impl SignerTrait for Signer { "push_result" => ?block_push_result, ); } + SignerMessage::MockProposal(mock_proposal) => { + let epoch = match stacks_client.get_node_epoch() { + Ok(epoch) => epoch, + Err(e) => { + warn!("{self}: Failed to determine node epoch. Cannot mock sign: {e}"); + return; + } + }; + info!("{self}: received a mock block proposal."; + "current_reward_cycle" => current_reward_cycle, + "epoch" => ?epoch + ); + if epoch == StacksEpochId::Epoch25 + && self.reward_cycle == current_reward_cycle + { + // We are in epoch 2.5, so we should mock sign to prove we are still alive. + self.mock_sign(mock_proposal.clone()); + } + } _ => {} } } @@ -184,22 +202,6 @@ impl SignerTrait for Signer { ); } *sortition_state = None; - let epoch = match stacks_client.get_node_epoch() { - Ok(epoch) => epoch, - Err(e) => { - warn!("{self}: Failed to determine node epoch. Cannot mock sign: {e}"); - return; - } - }; - debug!("{self}: Epoch 2.5 signer received a new burn block event."; - "burn_height" => burn_height, - "current_reward_cycle" => current_reward_cycle, - "epoch" => ?epoch - ); - if epoch == StacksEpochId::Epoch25 && self.reward_cycle == current_reward_cycle { - // We are in epoch 2.5, so we should mock mine to prove we are still alive. - self.mock_sign(*burn_height, stacks_client); - } } } } @@ -720,26 +722,9 @@ impl Signer { } /// Send a mock signature to stackerdb to prove we are still alive - fn mock_sign(&mut self, burn_block_height: u64, stacks_client: &StacksClient) { - let Ok(peer_info) = stacks_client.get_peer_info() else { - warn!("{self}: Failed to get peer info. Cannot mock sign."); - return; - }; - let chain_id = if self.mainnet { - CHAIN_ID_MAINNET - } else { - CHAIN_ID_TESTNET - }; - info!("Mock signing for burn block {burn_block_height:?}"; - "stacks_tip_consensus_hash" => ?peer_info.stacks_tip_consensus_hash.clone(), - "stacks_tip" => ?peer_info.stacks_tip.clone(), - "peer_burn_block_height" => peer_info.burn_block_height, - "pox_consensus" => ?peer_info.pox_consensus.clone(), - "server_version" => peer_info.server_version.clone(), - "chain_id" => chain_id - ); - let mock_signature = - MockSignature::new(burn_block_height, peer_info, chain_id, &self.private_key); + fn mock_sign(&mut self, mock_proposal: MockProposal) { + info!("{self}: Mock signing mock proposal: {mock_proposal:?}"); + let mock_signature = MockSignature::new(mock_proposal, &self.private_key); let message = SignerMessage::MockSignature(mock_signature); if let Err(e) = self .stackerdb diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 657315e993..8f856893ee 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -4191,7 +4191,6 @@ impl NakamotoChainState { "stackerdb_slots" => ?stackerdb_config.signers, "queried_sortition" => %election_sortition, "sortition_hashes" => ?miners_info.get_sortitions()); - return Ok(None); } let slot_id_range = signer_ranges.swap_remove(signer_ix); diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index 0ff56884a2..30a5990319 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -1168,6 +1168,10 @@ impl Config { .validate() .map_err(|e| format!("Atlas config error: {e}"))?; + if miner.mining_key.is_none() && miner.pre_nakamoto_mock_signing { + return Err("Cannot use pre_nakamoto_mock_signing without a mining_key".to_string()); + } + Ok(Config { config_path: config_file.__path, node, @@ -2352,6 +2356,8 @@ pub struct MinerConfig { pub max_reorg_depth: u64, /// Amount of time while mining in nakamoto to wait for signers to respond to a proposed block pub wait_on_signers: Duration, + /// Whether to mock sign in Epoch 2.5 through the .miners and .signers contracts. This is used for testing purposes in Epoch 2.5 only. + pub pre_nakamoto_mock_signing: bool, } impl Default for MinerConfig { @@ -2382,6 +2388,7 @@ impl Default for MinerConfig { max_reorg_depth: 3, // TODO: update to a sane value based on stackerdb benchmarking wait_on_signers: Duration::from_secs(200), + pre_nakamoto_mock_signing: false, // Should only default true if mining key is set } } } @@ -2731,10 +2738,17 @@ pub struct MinerConfigFile { pub filter_origins: Option, pub max_reorg_depth: Option, pub wait_on_signers_ms: Option, + pub pre_nakamoto_mock_signing: Option, } impl MinerConfigFile { fn into_config_default(self, miner_default_config: MinerConfig) -> Result { + let mining_key = self + .mining_key + .as_ref() + .map(|x| Secp256k1PrivateKey::from_hex(x)) + .transpose()?; + let pre_nakamoto_mock_signing = mining_key.is_some(); Ok(MinerConfig { first_attempt_time_ms: self .first_attempt_time_ms @@ -2833,6 +2847,9 @@ impl MinerConfigFile { .wait_on_signers_ms .map(Duration::from_millis) .unwrap_or(miner_default_config.wait_on_signers), + pre_nakamoto_mock_signing: self + .pre_nakamoto_mock_signing + .unwrap_or(pre_nakamoto_mock_signing), // Should only default true if mining key is set }) } } diff --git a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs index 0bba347795..6810afbb6b 100644 --- a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs +++ b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs @@ -860,8 +860,10 @@ impl SignCoordinator { debug!("Received block pushed message. Ignoring."); continue; } - SignerMessageV0::MockSignature(_) => { - debug!("Received mock signature message. Ignoring."); + SignerMessageV0::MockSignature(_) + | SignerMessageV0::MockProposal(_) + | SignerMessageV0::MockBlock(_) => { + debug!("Received mock message. Ignoring."); continue; } }; diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index afef51f47c..21ba451e6c 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -144,12 +144,17 @@ use std::io::{ErrorKind, Read, Write}; use std::net::SocketAddr; use std::sync::mpsc::{Receiver, TrySendError}; use std::thread::JoinHandle; -use std::time::Duration; +use std::time::{Duration, Instant}; use std::{fs, mem, thread}; +use clarity::boot_util::boot_code_id; use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use libsigner::v0::messages::{ + MessageSlotID, MinerSlotID, MockBlock, MockProposal, MockSignature, PeerInfo, SignerMessage, +}; +use libsigner::{SignerSession, StackerDBSession}; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; use stacks::burnchains::db::BurnchainHeaderReader; use stacks::burnchains::{Burnchain, BurnchainSigner, PoxConstants, Txid}; @@ -164,6 +169,7 @@ use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; use stacks::chainstate::coordinator::{get_next_recipients, OnChainRewardSetProvider}; use stacks::chainstate::nakamoto::NakamotoChainState; use stacks::chainstate::stacks::address::PoxAddress; +use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::db::blocks::StagingBlock; use stacks::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo, MINER_REWARD_MATURITY}; use stacks::chainstate::stacks::miner::{ @@ -179,18 +185,18 @@ use stacks::core::mempool::MemPoolDB; use stacks::core::{FIRST_BURNCHAIN_CONSENSUS_HASH, STACKS_EPOCH_3_0_MARKER}; use stacks::cost_estimates::metrics::{CostMetric, UnitMetric}; use stacks::cost_estimates::{CostEstimator, FeeEstimator, UnitEstimator}; -use stacks::monitoring; use stacks::monitoring::{increment_stx_blocks_mined_counter, update_active_miners_count_gauge}; use stacks::net::atlas::{AtlasConfig, AtlasDB}; use stacks::net::db::{LocalPeer, PeerDB}; use stacks::net::dns::{DNSClient, DNSResolver}; use stacks::net::p2p::PeerNetwork; use stacks::net::relay::Relayer; -use stacks::net::stackerdb::{StackerDBConfig, StackerDBSync, StackerDBs}; +use stacks::net::stackerdb::{StackerDBConfig, StackerDBSync, StackerDBs, MINER_SLOT_COUNT}; use stacks::net::{ Error as NetError, NetworkResult, PeerNetworkComms, RPCHandlerArgs, ServiceFlags, }; use stacks::util_lib::strings::{UrlString, VecDisplay}; +use stacks::{monitoring, version_string}; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, SortitionId, StacksAddress, StacksBlockId, @@ -211,6 +217,7 @@ use crate::burnchains::make_bitcoin_indexer; use crate::chain_data::MinerStats; use crate::config::NodeConfig; use crate::globals::{NeonGlobals as Globals, RelayerDirective}; +use crate::nakamoto_node::sign_coordinator::SignCoordinator; use crate::run_loop::neon::RunLoop; use crate::run_loop::RegisteredKey; use crate::ChainTip; @@ -2235,6 +2242,198 @@ impl BlockMinerThread { return false; } + /// Only used in mock signing to generate a peer info view + fn generate_peer_info(&self) -> PeerInfo { + // Create a peer info view of the current state + let server_version = version_string( + "stacks-node", + option_env!("STACKS_NODE_VERSION") + .or(option_env!("CARGO_PKG_VERSION")) + .unwrap_or("0.0.0.0"), + ); + let stacks_tip_height = self.burn_block.canonical_stacks_tip_height; + let stacks_tip = self.burn_block.canonical_stacks_tip_hash; + let stacks_tip_consensus_hash = self.burn_block.canonical_stacks_tip_consensus_hash; + let pox_consensus = self.burn_block.consensus_hash; + let burn_block_height = self.burn_block.block_height; + + PeerInfo { + burn_block_height, + stacks_tip_consensus_hash, + stacks_tip, + stacks_tip_height, + pox_consensus, + server_version, + } + } + + /// Only used in mock signing to retrieve the mock signatures for the given mock proposal + fn wait_for_mock_signatures( + &self, + mock_proposal: &MockProposal, + stackerdbs: &StackerDBs, + timeout: Duration, + ) -> Result, ChainstateError> { + let reward_cycle = self + .burnchain + .block_height_to_reward_cycle(self.burn_block.block_height) + .expect("BUG: block commit exists before first block height"); + let signers_contract_id = MessageSlotID::BlockResponse + .stacker_db_contract(self.config.is_mainnet(), reward_cycle); + let slot_ids: Vec<_> = stackerdbs + .get_signers(&signers_contract_id) + .expect("FATAL: could not get signers from stacker DB") + .into_iter() + .enumerate() + .map(|(slot_id, _)| { + u32::try_from(slot_id).expect("FATAL: too many signers to fit into u32 range") + }) + .collect(); + let mock_poll_start = Instant::now(); + let mut mock_signatures = vec![]; + // Because we don't care really if all signers reach quorum and this is just for testing purposes, + // we don't need to wait for ALL signers to sign the mock proposal and should not slow down mining too much + // Just wait a min amount of time for the mock signatures to come in + while mock_signatures.len() < slot_ids.len() && mock_poll_start.elapsed() < timeout { + let chunks = stackerdbs.get_latest_chunks(&signers_contract_id, &slot_ids)?; + for chunk in chunks { + if let Some(chunk) = chunk { + if let Ok(SignerMessage::MockSignature(mock_signature)) = + SignerMessage::consensus_deserialize(&mut chunk.as_slice()) + { + if mock_signature.mock_proposal == *mock_proposal + && !mock_signatures.contains(&mock_signature) + { + mock_signatures.push(mock_signature); + } + } + } + } + } + Ok(mock_signatures) + } + + /// Only used in mock signing to determine if the peer info view was already signed across + fn mock_block_exists(&self, peer_info: &PeerInfo) -> bool { + let miner_contract_id = boot_code_id(MINERS_NAME, self.config.is_mainnet()); + let mut miners_stackerdb = + StackerDBSession::new(&self.config.node.rpc_bind, miner_contract_id); + let miner_slot_ids: Vec<_> = (0..MINER_SLOT_COUNT * 2).collect(); + if let Ok(messages) = miners_stackerdb.get_latest_chunks(&miner_slot_ids) { + for message in messages { + if let Some(message) = message { + if message.is_empty() { + continue; + } + let Ok(SignerMessage::MockBlock(mock_block)) = + SignerMessage::consensus_deserialize(&mut message.as_slice()) + else { + continue; + }; + if mock_block.mock_proposal.peer_info == *peer_info { + return true; + } + } + } + } + false + } + + /// Read any mock signatures from stackerdb and respond to them + pub fn send_mock_miner_messages(&mut self) -> Result<(), ChainstateError> { + let miner_config = self.config.get_miner_config(); + if !miner_config.pre_nakamoto_mock_signing { + debug!("Pre-Nakamoto mock signing is disabled"); + return Ok(()); + } + + let burn_db_path = self.config.get_burn_db_file_path(); + let burn_db = SortitionDB::open(&burn_db_path, false, self.burnchain.pox_constants.clone()) + .expect("FATAL: could not open sortition DB"); + let epoch_id = SortitionDB::get_stacks_epoch(burn_db.conn(), self.burn_block.block_height)? + .expect("FATAL: no epoch defined") + .epoch_id; + if epoch_id != StacksEpochId::Epoch25 { + debug!("Mock miner messaging is disabled for non-epoch 2.5 blocks."; + "epoch_id" => epoch_id.to_string() + ); + return Ok(()); + } + + let mining_key = miner_config + .mining_key + .expect("Cannot mock sign without mining key"); + + // Create a peer info view of the current state + let peer_info = self.generate_peer_info(); + if self.mock_block_exists(&peer_info) { + debug!( + "Already sent mock miner block proposal for current peer info view. Not sending another mock proposal." + ); + return Ok(()); + } + + // find out which slot we're in. If we are not the latest sortition winner, we should not be sending anymore messages anyway + let stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), false)?; + let (_, miners_info) = + NakamotoChainState::make_miners_stackerdb_config(&burn_db, &self.burn_block)?; + let idx = miners_info.get_latest_winner_index(); + let sortitions = miners_info.get_sortitions(); + let election_sortition = *sortitions + .get(idx as usize) + .expect("FATAL: latest winner index out of bounds"); + + let miner_contract_id = boot_code_id(MINERS_NAME, self.config.is_mainnet()); + let mut miners_stackerdb = + StackerDBSession::new(&self.config.node.rpc_bind, miner_contract_id); + + let mock_proposal = + MockProposal::new(peer_info, self.config.burnchain.chain_id, &mining_key); + + info!("Sending mock proposal to stackerdb: {mock_proposal:?}"); + + if let Err(e) = SignCoordinator::send_miners_message( + &mining_key, + &burn_db, + &self.burn_block, + &stackerdbs, + SignerMessage::MockProposal(mock_proposal.clone()), + MinerSlotID::BlockProposal, // There is no specific slot for mock miner messages so we use BlockProposal for MockProposal as well. + self.config.is_mainnet(), + &mut miners_stackerdb, + &election_sortition, + ) { + warn!("Failed to send mock proposal to stackerdb: {:?}", &e); + return Ok(()); + } + + // Retrieve any MockSignatures from stackerdb + info!("Waiting for mock signatures..."); + let mock_signatures = + self.wait_for_mock_signatures(&mock_proposal, &stackerdbs, Duration::from_secs(10))?; + + let mock_block = MockBlock { + mock_proposal, + mock_signatures, + }; + + info!("Sending mock block to stackerdb: {mock_block:?}"); + if let Err(e) = SignCoordinator::send_miners_message( + &miner_config.mining_key.expect("BUG: no mining key"), + &burn_db, + &self.burn_block, + &stackerdbs, + SignerMessage::MockBlock(mock_block.clone()), + MinerSlotID::BlockPushed, // There is no specific slot for mock miner messages. Let's use BlockPushed for MockBlock since MockProposal uses BlockProposal. + self.config.is_mainnet(), + &mut miners_stackerdb, + &election_sortition, + ) { + warn!("Failed to send mock block to stackerdb: {:?}", &e); + } + Ok(()) + } + // TODO: add tests from mutation testing results #4871 #[cfg_attr(test, mutants::skip)] /// Try to mine a Stacks block by assembling one from mempool transactions and sending a @@ -3604,7 +3803,12 @@ impl RelayerThread { if let Ok(miner_handle) = thread::Builder::new() .name(format!("miner-block-{}", self.local_peer.data_url)) .stack_size(BLOCK_PROCESSOR_STACK_SIZE) - .spawn(move || miner_thread_state.run_tenure()) + .spawn(move || { + if let Err(e) = miner_thread_state.send_mock_miner_messages() { + warn!("Failed to send mock miner messages: {}", e); + } + miner_thread_state.run_tenure() + }) .inspect_err(|e| error!("Relayer: Failed to start tenure thread: {e:?}")) { self.miner_thread = Some(miner_handle); diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index c370ca53f6..24b7745419 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -391,7 +391,8 @@ pub fn get_latest_block_proposal( let message: SignerMessageV0 = miners_stackerdb.get_latest(miner_slot_id.start).ok()??; let SignerMessageV0::BlockProposal(block_proposal) = message else { - panic!("Expected a signer message block proposal. Got {message:?}"); + warn!("Expected a block proposal. Got {message:?}"); + return None; }; block_proposal.block }; diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index c2ce878e28..115fce4c83 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -51,7 +51,6 @@ use stacks_common::util::sleep_ms; use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView}; use stacks_signer::client::{SignerSlotID, StackerDB}; use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network}; -use stacks_signer::runloop::State; use stacks_signer::v0::SpawnedSigner; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; @@ -64,9 +63,8 @@ use crate::nakamoto_node::sign_coordinator::TEST_IGNORE_SIGNERS; use crate::neon::Counters; use crate::run_loop::boot_nakamoto; use crate::tests::nakamoto_integrations::{ - boot_to_epoch_25, boot_to_epoch_3_reward_set, boot_to_epoch_3_reward_set_calculation_boundary, - next_block_and, setup_epoch_3_reward_set, wait_for, POX_4_DEFAULT_STACKER_BALANCE, - POX_4_DEFAULT_STACKER_STX_AMT, + boot_to_epoch_25, boot_to_epoch_3_reward_set, next_block_and, setup_epoch_3_reward_set, + wait_for, POX_4_DEFAULT_STACKER_BALANCE, POX_4_DEFAULT_STACKER_STX_AMT, }; use crate::tests::neon_integrations::{ get_account, get_chain_info, next_block_and_wait, run_until_burnchain_height, submit_tx, @@ -2442,6 +2440,7 @@ fn mock_sign_epoch_25() { Some(Duration::from_secs(5)), |_| {}, |node_config| { + node_config.miner.pre_nakamoto_mock_signing = true; let epochs = node_config.burnchain.epochs.as_mut().unwrap(); for epoch in epochs.iter_mut() { if epoch.epoch_id == StacksEpochId::Epoch25 { @@ -2463,35 +2462,36 @@ fn mock_sign_epoch_25() { .clone() .unwrap(); let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()]; - let epoch_3_start_height = epoch_3.start_height; + let epoch_3_boundary = epoch_3.start_height - 1; // We only advance to the boundary as epoch 2.5 miner gets torn down at the boundary signer_test.boot_to_epoch_25_reward_cycle(); info!("------------------------- Test Processing Epoch 2.5 Tenures -------------------------"); // Mine until epoch 3.0 and ensure that no more mock signatures are received - let mut reward_cycle = signer_test.get_current_reward_cycle(); - let mut stackerdb = StackerDB::new( - &signer_test.running_nodes.conf.node.rpc_bind, - StacksPrivateKey::new(), // We are just reading so don't care what the key is - false, - reward_cycle, - SignerSlotID(0), // We are just reading so again, don't care about index. - ); - let mut signer_slot_ids: Vec<_> = signer_test + let reward_cycle = signer_test.get_current_reward_cycle(); + let signer_slot_ids: Vec<_> = signer_test .get_signer_indices(reward_cycle) .iter() .map(|id| id.0) .collect(); + let signer_keys = signer_test.get_signer_public_keys(reward_cycle); + let signer_public_keys: Vec<_> = signer_keys.signers.into_values().collect(); assert_eq!(signer_slot_ids.len(), num_signers); - // Mine until epoch 3.0 and ensure we get a new mock signature per epoch 2.5 sortition + + let miners_stackerdb_contract = boot_code_id(MINERS_NAME, false); + + // Mine until epoch 3.0 and ensure we get a new mock block per epoch 2.5 sortition let main_poll_time = Instant::now(); + // Only advance to the boundary as the epoch 2.5 miner will be shut down at this point. while signer_test .running_nodes .btc_regtest_controller .get_headers_height() - < epoch_3_start_height + < epoch_3_boundary { + let mut mock_block_mesage = None; + let mock_poll_time = Instant::now(); next_block_and( &mut signer_test.running_nodes.btc_regtest_controller, 60, @@ -2502,108 +2502,59 @@ fn mock_sign_epoch_25() { .running_nodes .btc_regtest_controller .get_headers_height(); - if current_burn_block_height - % signer_test - .running_nodes - .conf - .get_burnchain() - .pox_constants - .reward_cycle_length as u64 - == 0 - { - reward_cycle += 1; - debug!("Rolling over reward cycle to {:?}", reward_cycle); - stackerdb = StackerDB::new( - &signer_test.running_nodes.conf.node.rpc_bind, - StacksPrivateKey::new(), // We are just reading so don't care what the key is - false, - reward_cycle, - SignerSlotID(0), // We are just reading so again, don't care about index. - ); - signer_slot_ids = signer_test - .get_signer_indices(reward_cycle) - .iter() - .map(|id| id.0) - .collect(); - assert_eq!(signer_slot_ids.len(), num_signers); - } - let mut mock_signatures = vec![]; - let mock_poll_time = Instant::now(); - debug!("Waiting for mock signatures for burn block height {current_burn_block_height}"); - while mock_signatures.len() != num_signers { + debug!("Waiting for mock miner message for burn block height {current_burn_block_height}"); + while mock_block_mesage.is_none() { std::thread::sleep(Duration::from_millis(100)); - let messages: Vec = StackerDB::get_messages( - stackerdb - .get_session_mut(&MessageSlotID::MockSignature) - .expect("Failed to get BlockResponse stackerdb session"), - &signer_slot_ids, - ) - .expect("Failed to get message from stackerdb"); - for message in messages { - if let SignerMessage::MockSignature(mock_signature) = message { - if mock_signature.sign_data.event_burn_block_height == current_burn_block_height - { - if !mock_signatures.contains(&mock_signature) { - mock_signatures.push(mock_signature); - } + let chunks = test_observer::get_stackerdb_chunks(); + for chunk in chunks + .into_iter() + .filter_map(|chunk| { + if chunk.contract_id != miners_stackerdb_contract { + return None; } + Some(chunk.modified_slots) + }) + .flatten() + { + if chunk.data.is_empty() { + continue; + } + let SignerMessage::MockBlock(mock_block) = + SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) + .expect("Failed to deserialize SignerMessage") + else { + continue; + }; + if mock_block.mock_proposal.peer_info.burn_block_height == current_burn_block_height + { + assert_eq!(mock_block.mock_signatures.len(), num_signers); + mock_block + .mock_signatures + .iter() + .for_each(|mock_signature| { + assert!(signer_public_keys.iter().any(|signer| { + mock_signature + .verify( + &StacksPublicKey::from_slice(signer.to_bytes().as_slice()) + .unwrap(), + ) + .expect("Failed to verify mock signature") + })); + }); + mock_block_mesage = Some(mock_block); + break; } } assert!( mock_poll_time.elapsed() <= Duration::from_secs(15), - "Failed to find mock signatures within timeout" + "Failed to find mock miner message within timeout" ); } assert!( main_poll_time.elapsed() <= Duration::from_secs(45), - "Timed out waiting to advance epoch 3.0" + "Timed out waiting to advance epoch 3.0 boundary" ); } - - info!("------------------------- Test Processing Epoch 3.0 Tenure -------------------------"); - let old_messages: Vec = StackerDB::get_messages( - stackerdb - .get_session_mut(&MessageSlotID::MockSignature) - .expect("Failed to get BlockResponse stackerdb session"), - &signer_slot_ids, - ) - .expect("Failed to get message from stackerdb"); - let old_signatures = old_messages - .iter() - .filter_map(|message| { - if let SignerMessage::MockSignature(mock_signature) = message { - Some(mock_signature) - } else { - None - } - }) - .collect::>(); - next_block_and( - &mut signer_test.running_nodes.btc_regtest_controller, - 60, - || Ok(true), - ) - .unwrap(); - // Wait a bit to ensure no new mock signatures show up - std::thread::sleep(Duration::from_secs(5)); - let new_messages: Vec = StackerDB::get_messages( - stackerdb - .get_session_mut(&MessageSlotID::MockSignature) - .expect("Failed to get BlockResponse stackerdb session"), - &signer_slot_ids, - ) - .expect("Failed to get message from stackerdb"); - let new_signatures = new_messages - .iter() - .filter_map(|message| { - if let SignerMessage::MockSignature(mock_signature) = message { - Some(mock_signature) - } else { - None - } - }) - .collect::>(); - assert_eq!(old_signatures, new_signatures); } #[test]