Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/mock signing in 2.5 #5020

Merged
merged 9 commits into from
Jul 31, 2024
133 changes: 131 additions & 2 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,15 @@ use blockstack_lib::net::api::postblock_proposal::{
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
};
use blockstack_lib::util_lib::boot::boot_code_id;
use clarity::types::chainstate::{ConsensusHash, StacksPrivateKey, StacksPublicKey};
use clarity::types::PrivateKey;
use clarity::util::retry::BoundReader;
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::types::serialization::SerializationError;
use clarity::vm::types::QualifiedContractIdentifier;
use hashbrown::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512_256};
use stacks_common::codec::{
read_next, read_next_at_most, read_next_exact, write_next, Error as CodecError,
StacksMessageCodec,
Expand All @@ -55,6 +58,7 @@ use tiny_http::{
};

use crate::http::{decode_http_body, decode_http_request};
use crate::stacks_common::types::PublicKey;
use crate::{
BlockProposal, EventError, MessageSlotID as MessageSlotIDTrait,
SignerMessage as SignerMessageTrait,
Expand All @@ -65,7 +69,9 @@ 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
BlockResponse = 1,
/// Mock Signature message from Epoch 2.5 signers
MockSignature = 2
});

define_u8_enum!(
Expand Down Expand Up @@ -100,7 +106,9 @@ SignerMessageTypePrefix {
/// Block Response message from signers
BlockResponse = 1,
/// Block Pushed message from miners
BlockPushed = 2
BlockPushed = 2,
/// Mock Signature message from Epoch 2.5 signers
MockSignature = 3
});

#[cfg_attr(test, mutants::skip)]
Expand Down Expand Up @@ -143,6 +151,7 @@ impl From<&SignerMessage> for SignerMessageTypePrefix {
SignerMessage::BlockProposal(_) => SignerMessageTypePrefix::BlockProposal,
SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse,
SignerMessage::BlockPushed(_) => SignerMessageTypePrefix::BlockPushed,
SignerMessage::MockSignature(_) => SignerMessageTypePrefix::MockSignature,
}
}
}
Expand All @@ -156,6 +165,8 @@ pub enum SignerMessage {
BlockResponse(BlockResponse),
/// A block pushed from miners to the signers set
BlockPushed(NakamotoBlock),
/// A mock signature from the epoch 2.5 signers
MockSignature(MockSignature),
}

impl SignerMessage {
Expand All @@ -167,6 +178,7 @@ impl SignerMessage {
match self {
Self::BlockProposal(_) | Self::BlockPushed(_) => None,
Self::BlockResponse(_) => Some(MessageSlotID::BlockResponse),
Self::MockSignature(_) => Some(MessageSlotID::MockSignature),
}
}
}
Expand All @@ -180,6 +192,7 @@ impl StacksMessageCodec for SignerMessage {
SignerMessage::BlockProposal(block_proposal) => block_proposal.consensus_serialize(fd),
SignerMessage::BlockResponse(block_response) => block_response.consensus_serialize(fd),
SignerMessage::BlockPushed(block) => block.consensus_serialize(fd),
SignerMessage::MockSignature(signature) => signature.consensus_serialize(fd),
}?;
Ok(())
}
Expand All @@ -201,6 +214,10 @@ impl StacksMessageCodec for SignerMessage {
let block = StacksMessageCodec::consensus_deserialize(fd)?;
SignerMessage::BlockPushed(block)
}
SignerMessageTypePrefix::MockSignature => {
let signature = StacksMessageCodec::consensus_deserialize(fd)?;
SignerMessage::MockSignature(signature)
}
};
Ok(message)
}
Expand All @@ -214,6 +231,73 @@ pub trait StacksMessageCodecExtensions: Sized {
fn inner_consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError>;
}

/// A signer's mock signature across its last seen Stacks Consensus Hash. 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 across the stacks consensus hash
signature: MessageSignature,
/// The block hash that the signature is across
pub stacks_consensus_hash: ConsensusHash,
}

impl MockSignature {
/// Create a new mock signature with the provided stacks consensus hash and private key
pub fn new(
stacks_consensus_hash: ConsensusHash,
stacks_private_key: &StacksPrivateKey,
) -> Self {
let mut sig = Self {
signature: MessageSignature::empty(),
stacks_consensus_hash,
};
sig.sign(stacks_private_key)
.expect("Failed to sign MockSignature");
sig
}

/// The signature hash for the mock signature
pub fn signature_hash(&self) -> Result<Sha512Trunc256Sum, CodecError> {
let mut hasher = Sha512_256::new();
let fd = &mut hasher;
write_next(fd, &self.stacks_consensus_hash)?;
Ok(Sha512Trunc256Sum::from_hasher(hasher))
}
jferrant marked this conversation as resolved.
Show resolved Hide resolved
/// 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().map_err(|e| e.to_string())?;
self.signature = private_key.sign(&signature_hash.0)?;
Ok(())
}
/// Verify the mock signature against the provided public key
pub fn verify(&self, public_key: &StacksPublicKey) -> Result<bool, String> {
if self.signature == MessageSignature::empty() {
return Ok(false);
}
let signature_hash = self.signature_hash().map_err(|e| e.to_string())?;
public_key
.verify(&signature_hash.0, &self.signature)
.map_err(|e| e.to_string())
}
}

impl StacksMessageCodec for MockSignature {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
write_next(fd, &self.signature)?;
write_next(fd, &self.stacks_consensus_hash)?;
Ok(())
}

fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
let signature = read_next::<MessageSignature, _>(fd)?;
let stacks_consensus_hash = read_next::<ConsensusHash, _>(fd)?;
Ok(Self {
signature,
stacks_consensus_hash,
})
}
}

define_u8_enum!(
/// Enum representing the reject code type prefix
RejectCodeTypePrefix {
Expand Down Expand Up @@ -508,6 +592,7 @@ mod test {
};
use blockstack_lib::util_lib::strings::StacksString;
use clarity::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash};
use clarity::types::PrivateKey;
use clarity::util::hash::MerkleTree;
use clarity::util::secp256k1::MessageSignature;
use rand::{thread_rng, Rng, RngCore};
Expand Down Expand Up @@ -622,4 +707,48 @@ mod test {
.expect("Failed to deserialize SignerMessage");
assert_eq!(signer_message, deserialized_signer_message);
}

#[test]
fn verify_sign_mock_signature() {
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 byte: u8 = thread_rng().gen();
let stacks_consensus_hash = ConsensusHash([byte; 20]);
let mut mock_signature = MockSignature {
signature: MessageSignature::empty(),
stacks_consensus_hash,
};
assert!(!mock_signature
.verify(&public_key)
.expect("Failed to verify MockSignature"));

mock_signature
.sign(&private_key)
.expect("Failed to sign MockSignature");

assert!(mock_signature
.verify(&public_key)
.expect("Failed to verify MockSignature"));
assert!(!mock_signature
.verify(&bad_public_key)
.expect("Failed to verify MockSignature"));
}

#[test]
fn serde_mock_signature() {
let byte: u8 = thread_rng().gen();
let stacks_consensus_hash = ConsensusHash([byte; 20]);
let mock_signature = MockSignature {
signature: MessageSignature::empty(),
stacks_consensus_hash,
};
let serialized_signature = mock_signature.serialize_to_vec();
let deserialized_signature = read_next::<MockSignature, _>(&mut &serialized_signature[..])
.expect("Failed to deserialize MockSignature");
assert_eq!(mock_signature, deserialized_signature);
}
}
35 changes: 31 additions & 4 deletions stacks-signer/src/v0/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ use std::sync::mpsc::Sender;

use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse;
use clarity::types::chainstate::StacksPrivateKey;
use clarity::types::PrivateKey;
use clarity::types::{PrivateKey, StacksEpochId};
use clarity::util::hash::MerkleHashFunc;
use clarity::util::secp256k1::Secp256k1PublicKey;
use libsigner::v0::messages::{BlockResponse, MessageSlotID, RejectCode, SignerMessage};
use libsigner::v0::messages::{
BlockResponse, MessageSlotID, MockSignature, RejectCode, SignerMessage,
};
use libsigner::{BlockProposal, SignerEvent};
use slog::{slog_debug, slog_error, slog_info, slog_warn};
use stacks_common::types::chainstate::StacksAddress;
Expand Down Expand Up @@ -84,7 +86,7 @@ impl SignerTrait<SignerMessage> for Signer {
sortition_state: &mut Option<SortitionsView>,
event: Option<&SignerEvent<SignerMessage>>,
_res: Sender<Vec<SignerResult>>,
_current_reward_cycle: u64,
current_reward_cycle: u64,
) {
let event_parity = match event {
// Block proposal events do have reward cycles, but each proposal has its own cycle,
Expand Down Expand Up @@ -153,7 +155,7 @@ impl SignerTrait<SignerMessage> for Signer {
burn_header_hash,
received_time,
} => {
debug!("{self}: Receved a new burn block event for block height {burn_height}");
debug!("{self}: Received a new burn block event for block height {burn_height}");
if let Err(e) =
self.signer_db
.insert_burn_block(burn_header_hash, *burn_height, received_time)
Expand All @@ -166,6 +168,13 @@ impl SignerTrait<SignerMessage> for Signer {
);
}
*sortition_state = None;
if let Ok(StacksEpochId::Epoch25) = stacks_client.get_node_epoch() {
if self.reward_cycle == current_reward_cycle {
// We are in epoch 2.5, so we should mock mine to prove we are still alive.
debug!("Mock signing for burn block {burn_height:?}");
self.mock_sign(stacks_client);
}
};
}
}
}
Expand Down Expand Up @@ -462,4 +471,22 @@ impl Signer {
.insert_block(&block_info)
.unwrap_or_else(|_| panic!("{self}: Failed to insert block in DB"));
}

/// Send a mock signature to stackerdb to prove we are still alive
fn mock_sign(&mut self, stacks_client: &StacksClient) {
let Ok(peer_info) = stacks_client.get_peer_info() else {
warn!("{self}: Failed to get peer info. Cannot mock mine.");
return;
};
let consensus_hash = peer_info.stacks_tip_consensus_hash;
debug!("Mock signing using stacks tip {consensus_hash:?}");
let mock_signature = MockSignature::new(consensus_hash, &self.private_key);
let message = SignerMessage::MockSignature(mock_signature);
if let Err(e) = self
.stackerdb
.send_message_with_retry::<SignerMessage>(message)
{
warn!("{self}: Failed to send mock signature to stacker-db: {e:?}",);
}
}
}
4 changes: 4 additions & 0 deletions testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,10 @@ impl SignCoordinator {
debug!("Received block pushed message. Ignoring.");
continue;
}
SignerMessageV0::MockSignature(_) => {
debug!("Received mock signature message. Ignoring.");
continue;
}
};
let block_sighash = block.header.signer_signature_hash();
if block_sighash != response_hash {
Expand Down
55 changes: 48 additions & 7 deletions testnet/stacks-node/src/tests/nakamoto_integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ use crate::tests::{
use crate::{tests, BitcoinRegtestController, BurnchainController, Config, ConfigFile, Keychain};

pub static POX_4_DEFAULT_STACKER_BALANCE: u64 = 100_000_000_000_000;
static POX_4_DEFAULT_STACKER_STX_AMT: u128 = 99_000_000_000_000;
pub static POX_4_DEFAULT_STACKER_STX_AMT: u128 = 99_000_000_000_000;

lazy_static! {
pub static ref NAKAMOTO_INTEGRATION_EPOCHS: [StacksEpoch; 9] = [
Expand Down Expand Up @@ -166,13 +166,13 @@ lazy_static! {
StacksEpoch {
epoch_id: StacksEpochId::Epoch25,
start_height: 201,
end_height: 231,
end_height: 251,
block_limit: HELIUM_BLOCK_LIMIT_20.clone(),
network_epoch: PEER_VERSION_EPOCH_2_5
},
StacksEpoch {
epoch_id: StacksEpochId::Epoch30,
start_height: 231,
start_height: 251,
jferrant marked this conversation as resolved.
Show resolved Hide resolved
end_height: STACKS_EPOCH_MAX,
block_limit: HELIUM_BLOCK_LIMIT_20.clone(),
network_epoch: PEER_VERSION_EPOCH_3_0
Expand Down Expand Up @@ -621,9 +621,9 @@ pub fn boot_to_epoch_3(

let epochs = naka_conf.burnchain.epochs.clone().unwrap();
let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()];

let current_height = btc_regtest_controller.get_headers_height();
info!(
"Chain bootstrapped to bitcoin block 201, starting Epoch 2x miner";
"Chain bootstrapped to bitcoin block {current_height:?}, starting Epoch 2x miner";
"Epoch 3.0 Boundary" => (epoch_3.start_height - 1),
);
let http_origin = format!("http://{}", &naka_conf.node.rpc_bind);
Expand Down Expand Up @@ -995,6 +995,47 @@ pub fn boot_to_epoch_3_reward_set_calculation_boundary(
info!("Bootstrapped to Epoch 3.0 reward set calculation boundary height: {epoch_3_reward_set_calculation_boundary}.");
}

///
/// * `stacker_sks` - must be a private key for sending a large `stack-stx` transaction in order
/// for pox-4 to activate
/// * `signer_pks` - must be the same size as `stacker_sks`
pub fn boot_to_epoch_25(
naka_conf: &Config,
blocks_processed: &Arc<AtomicU64>,
btc_regtest_controller: &mut BitcoinRegtestController,
) {
let epochs = naka_conf.burnchain.epochs.clone().unwrap();
let epoch_25 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch25).unwrap()];
let reward_cycle_len = naka_conf.get_burnchain().pox_constants.reward_cycle_length as u64;
let prepare_phase_len = naka_conf.get_burnchain().pox_constants.prepare_length as u64;

let epoch_25_start_height = epoch_25.start_height;
assert!(
epoch_25_start_height > 0,
"Epoch 2.5 start height must be greater than 0"
);
// stack enough to activate pox-4
let block_height = btc_regtest_controller.get_headers_height();
let reward_cycle = btc_regtest_controller
.get_burnchain()
.block_height_to_reward_cycle(block_height)
.unwrap();
debug!("Test Cycle Info";
"prepare_phase_len" => {prepare_phase_len},
"reward_cycle_len" => {reward_cycle_len},
"block_height" => {block_height},
"reward_cycle" => {reward_cycle},
"epoch_25_start_height" => {epoch_25_start_height},
);
run_until_burnchain_height(
btc_regtest_controller,
&blocks_processed,
epoch_25_start_height,
&naka_conf,
);
info!("Bootstrapped to Epoch 2.5: {epoch_25_start_height}.");
}

///
/// * `stacker_sks` - must be a private key for sending a large `stack-stx` transaction in order
/// for pox-4 to activate
Expand Down Expand Up @@ -1517,9 +1558,9 @@ fn correct_burn_outs() {
let epochs = naka_conf.burnchain.epochs.clone().unwrap();
let epoch_3 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap()];
let epoch_25 = &epochs[StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch25).unwrap()];

let current_height = btc_regtest_controller.get_headers_height();
info!(
"Chain bootstrapped to bitcoin block 201, starting Epoch 2x miner";
"Chain bootstrapped to bitcoin block {current_height:?}, starting Epoch 2x miner";
"Epoch 3.0 Boundary" => (epoch_3.start_height - 1),
);

Expand Down
Loading