Merge pull request #4527 from stacks-network/dream-team-fixes

Fixes discovered while bringing up the pre-launch testnet
This commit is contained in:
Matthew Little
2024-03-15 17:54:06 +00:00
committed by GitHub
13 changed files with 395 additions and 153 deletions

View File

@@ -50,11 +50,22 @@ use wsts::state_machine::signer;
use crate::http::{decode_http_body, decode_http_request};
use crate::{EventError, SignerMessage};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// BlockProposal sent to signers
pub struct BlockProposalSigners {
/// The block itself
pub block: NakamotoBlock,
/// The burn height the block is mined during
pub burn_height: u64,
/// The reward cycle the block is mined during
pub reward_cycle: u64,
}
/// Event enum for newly-arrived signer subscribed events
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum SignerEvent {
/// The miner proposed blocks for signers to observe and sign
ProposedBlocks(Vec<NakamotoBlock>),
ProposedBlocks(Vec<BlockProposalSigners>),
/// The signer messages for other signers and miners to observe
/// The u32 is the signer set to which the message belongs (either 0 or 1)
SignerMessages(u32, Vec<SignerMessage>),
@@ -64,6 +75,26 @@ pub enum SignerEvent {
StatusCheck,
}
impl StacksMessageCodec for BlockProposalSigners {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
self.block.consensus_serialize(fd)?;
self.burn_height.consensus_serialize(fd)?;
self.reward_cycle.consensus_serialize(fd)?;
Ok(())
}
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
let block = NakamotoBlock::consensus_deserialize(fd)?;
let burn_height = u64::consensus_deserialize(fd)?;
let reward_cycle = u64::consensus_deserialize(fd)?;
Ok(BlockProposalSigners {
block,
burn_height,
reward_cycle,
})
}
}
/// Trait to implement a stop-signaler for the event receiver thread.
/// The caller calls `send()` and the event receiver loop (which lives in a separate thread) will
/// terminate.
@@ -195,11 +226,15 @@ impl EventStopSignaler for SignerStopSignaler {
// We need to send actual data to trigger the event receiver
let body = "Yo. Shut this shit down!".to_string();
let req = format!(
"POST /shutdown HTTP/1.0\r\nContent-Length: {}\r\n\r\n{}",
&body.len(),
"POST /shutdown HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nContent-Length: {}\r\nContent-Type: text/plain\r\n\r\n{}",
self.local_addr,
body.len(),
body
);
stream.write_all(req.as_bytes()).unwrap();
match stream.write_all(req.as_bytes()) {
Err(e) => error!("Failed to send shutdown request: {}", e),
_ => (),
};
}
}
}
@@ -337,10 +372,10 @@ fn process_stackerdb_event(
.map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?;
let signer_event = if event.contract_id == boot_code_id(MINERS_NAME, is_mainnet) {
let blocks: Vec<NakamotoBlock> = event
let blocks: Vec<BlockProposalSigners> = event
.modified_slots
.iter()
.filter_map(|chunk| read_next::<NakamotoBlock, _>(&mut &chunk.data[..]).ok())
.filter_map(|chunk| read_next::<BlockProposalSigners, _>(&mut &chunk.data[..]).ok())
.collect();
SignerEvent::ProposedBlocks(blocks)
} else if event.contract_id.name.to_string().starts_with(SIGNERS_NAME)

View File

@@ -238,12 +238,12 @@ pub fn run_http_request<S: Read + Write>(
let req_txt = if let Some(content_type) = content_type {
format!(
"{} {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\nContent-Type: {}\r\n{}User-Agent: libsigner/0.1\r\nAccept: */*\r\n\r\n",
"{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nContent-Type: {}\r\n{}User-Agent: libsigner/0.1\r\nAccept: */*\r\n\r\n",
verb, path, host, content_type, content_length_hdr
)
} else {
format!(
"{} {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n{}User-Agent: libsigner/0.1\r\nAccept: */*\r\n\r\n",
"{} {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n{}User-Agent: libsigner/0.1\r\nAccept: */*\r\n\r\n",
verb, path, host, content_length_hdr
)
};

View File

@@ -45,7 +45,8 @@ mod session;
pub use crate::error::{EventError, RPCError};
pub use crate::events::{
EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, SignerStopSignaler,
BlockProposalSigners, EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver,
SignerStopSignaler,
};
pub use crate::messages::{
BlockRejection, BlockResponse, RejectCode, SignerMessage, BLOCK_MSG_ID, TRANSACTIONS_MSG_ID,

View File

@@ -823,6 +823,27 @@ pub enum BlockResponse {
Rejected(BlockRejection),
}
impl std::fmt::Display for BlockResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlockResponse::Accepted(a) => {
write!(
f,
"BlockAccepted: signer_sighash = {}, signature = {}",
a.0, a.1
)
}
BlockResponse::Rejected(r) => {
write!(
f,
"BlockRejected: signer_sighash = {}, code = {}, reason = {}",
r.reason_code, r.reason, r.signer_signature_hash
)
}
}
}
}
impl BlockResponse {
/// Create a new accepted BlockResponse for the provided block signer signature hash and signature
pub fn accepted(hash: Sha512Trunc256Sum, sig: Signature) -> Self {

View File

@@ -135,7 +135,12 @@ fn test_simple_signer() {
let ev = &thread_chunks[num_sent];
let body = serde_json::to_string(ev).unwrap();
let req = format!("POST /stackerdb_chunks HTTP/1.0\r\nConnection: close\r\nContent-Length: {}\r\n\r\n{}", &body.len(), body);
let req = format!(
"POST /stackerdb_chunks HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
endpoint,
&body.len(),
body
);
debug!("Send:\n{}", &req);
sock.write_all(req.as_bytes()).unwrap();
@@ -188,13 +193,16 @@ fn test_status_endpoint() {
return;
}
};
let req = "GET /status HTTP/1.0\r\nConnection: close\r\n\r\n";
let req = format!(
"GET /status HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
endpoint
);
sock.write_all(req.as_bytes()).unwrap();
let mut buf = [0; 128];
let _ = sock.read(&mut buf).unwrap();
let res_str = std::str::from_utf8(&buf).unwrap();
let expected_status_res = "HTTP/1.0 200 OK\r\n";
let expected_status_res = "HTTP/1.1 200 OK\r\n";
assert_eq!(expected_status_res, &res_str[..expected_status_res.len()]);
sock.flush().unwrap();
});

View File

@@ -69,6 +69,9 @@ impl From<PublicKeys> for CoordinatorSelector {
}
}
/// Whether or not to rotate to new coordinators in `update_coordinator`
const ROTATE_COORDINATORS: bool = false;
impl CoordinatorSelector {
/// Update the coordinator id
fn update_coordinator(&mut self, new_coordinator_ids: Vec<u32>) {
@@ -81,7 +84,7 @@ impl CoordinatorSelector {
.coordinator_ids
.first()
.expect("FATAL: No registered signers");
if new_coordinator_id == self.coordinator_id {
if ROTATE_COORDINATORS && new_coordinator_id == self.coordinator_id {
// If the newly selected coordinator is the same as the current and we have more than one available, advance immediately to the next
if self.coordinator_ids.len() > 1 {
new_index = new_index.saturating_add(1);
@@ -89,12 +92,16 @@ impl CoordinatorSelector {
}
new_index
} else {
let mut new_index = self.coordinator_index.saturating_add(1);
if new_index == self.coordinator_ids.len() {
// We have exhausted all potential coordinators. Go back to the start
new_index = 0;
if ROTATE_COORDINATORS {
let mut new_index = self.coordinator_index.saturating_add(1);
if new_index == self.coordinator_ids.len() {
// We have exhausted all potential coordinators. Go back to the start
new_index = 0;
}
new_index
} else {
self.coordinator_index
}
new_index
};
self.coordinator_id = *self
.coordinator_ids
@@ -136,7 +143,7 @@ impl CoordinatorSelector {
)
}
/// Calculate the ordered list of coordinator ids by comparing the provided public keys against the pox consensus hash
/// Calculate the ordered list of coordinator ids by comparing the provided public keys
pub fn calculate_coordinator_ids(
public_keys: &PublicKeys,
pox_consensus_hash: &ConsensusHash,

View File

@@ -229,7 +229,7 @@ impl RunLoop {
}
/// Refresh signer configuration for a specific reward cycle
fn refresh_signer_config(&mut self, reward_cycle: u64) {
fn refresh_signer_config(&mut self, reward_cycle: u64, current: bool) {
let reward_index = reward_cycle % 2;
let mut needs_refresh = false;
if let Some(signer) = self.stacks_signers.get_mut(&reward_index) {
@@ -266,7 +266,12 @@ impl RunLoop {
.insert(reward_index, Signer::from(new_signer_config));
debug!("Reward cycle #{reward_cycle} Signer #{signer_id} initialized.");
} else {
warn!("Signer is not registered for reward cycle {reward_cycle}. Waiting for confirmed registration...");
// TODO: Update `current` here once the signer binary is tracking its own latest burnchain/stacks views.
if current {
warn!("Signer is not registered for the current reward cycle ({reward_cycle}). Waiting for confirmed registration...");
} else {
debug!("Signer is not registered for reward cycle {reward_cycle}. Waiting for confirmed registration...");
}
}
}
}
@@ -275,11 +280,19 @@ impl RunLoop {
/// Note: this will trigger DKG if required
fn refresh_signers(&mut self, current_reward_cycle: u64) -> Result<(), ClientError> {
let next_reward_cycle = current_reward_cycle.saturating_add(1);
self.refresh_signer_config(current_reward_cycle);
self.refresh_signer_config(next_reward_cycle);
self.refresh_signer_config(current_reward_cycle, true);
self.refresh_signer_config(next_reward_cycle, false);
// TODO: do not use an empty consensus hash
let pox_consensus_hash = ConsensusHash::empty();
for signer in self.stacks_signers.values_mut() {
let mut to_delete = Vec::new();
for (idx, signer) in &mut self.stacks_signers {
if signer.reward_cycle < current_reward_cycle {
debug!("{signer}: Signer's tenure has completed.");
// We don't really need this state, but it's useful for debugging
signer.state = SignerState::TenureCompleted;
to_delete.push(*idx);
continue;
}
let old_coordinator_id = signer.coordinator_selector.get_coordinator().0;
let updated_coordinator_id = signer
.coordinator_selector
@@ -302,6 +315,11 @@ impl RunLoop {
})?;
}
}
for i in to_delete.into_iter() {
if let Some(signer) = self.stacks_signers.remove(&i) {
info!("{signer}: Tenure has completed. Removing signer from runloop.",);
}
}
if self.stacks_signers.is_empty() {
info!("Signer is not registered for the current reward cycle ({current_reward_cycle}) or next reward cycle ({next_reward_cycle}). Waiting for confirmed registration...");
self.state = State::Uninitialized;
@@ -357,6 +375,26 @@ impl SignerRunLoop<Vec<OperationResult>, RunLoopCommand> for RunLoop {
error!("Failed to refresh signers: {e}. Signer may have an outdated view of the network. Attempting to process event anyway.");
}
for signer in self.stacks_signers.values_mut() {
if signer.state == SignerState::TenureCompleted {
warn!("{signer}: Signer's tenure has completed. This signer should have been cleaned up during refresh.");
continue;
}
let event_parity = match event {
Some(SignerEvent::BlockValidationResponse(_)) => Some(current_reward_cycle % 2),
// Block proposal events do have reward cycles, but each proposal has its own cycle,
// and the vec could be heterogenous, so, don't differentiate.
Some(SignerEvent::ProposedBlocks(_)) => None,
Some(SignerEvent::SignerMessages(msg_parity, ..)) => {
Some(u64::from(msg_parity) % 2)
}
Some(SignerEvent::StatusCheck) => None,
None => None,
};
let other_signer_parity = (signer.reward_cycle + 1) % 2;
if event_parity == Some(other_signer_parity) {
continue;
}
if let Err(e) = signer.process_event(
&self.stacks_client,
event.as_ref(),

View File

@@ -24,7 +24,9 @@ use blockstack_lib::chainstate::stacks::boot::SIGNERS_VOTING_FUNCTION_NAME;
use blockstack_lib::chainstate::stacks::StacksTransaction;
use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse;
use hashbrown::HashSet;
use libsigner::{BlockRejection, BlockResponse, RejectCode, SignerEvent, SignerMessage};
use libsigner::{
BlockProposalSigners, BlockRejection, BlockResponse, RejectCode, SignerEvent, SignerMessage,
};
use serde_derive::{Deserialize, Serialize};
use slog::{slog_debug, slog_error, slog_info, slog_warn};
use stacks_common::codec::{read_next, StacksMessageCodec};
@@ -71,7 +73,7 @@ pub struct BlockInfo {
/// The associated packet nonce request if we have one
nonce_request: Option<NonceRequest>,
/// Whether this block is already being signed over
signed_over: bool,
pub signed_over: bool,
}
impl BlockInfo {
@@ -126,6 +128,8 @@ pub enum State {
Idle,
/// The signer is executing a DKG or Sign round
OperationInProgress,
/// The signer's reward cycle has finished
TenureCompleted,
}
/// The stacks signer registered for the reward cycle
@@ -170,8 +174,10 @@ impl std::fmt::Display for Signer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Reward Cycle #{} Signer #{}",
self.reward_cycle, self.signer_id,
"Cycle #{} Signer #{}(C:{})",
self.reward_cycle,
self.signer_id,
self.coordinator_selector.get_coordinator().0,
)
}
}
@@ -311,7 +317,7 @@ impl Signer {
let signer_signature_hash = block.header.signer_signature_hash();
let mut block_info = self
.signer_db
.block_lookup(&signer_signature_hash)
.block_lookup(self.reward_cycle, &signer_signature_hash)
.unwrap_or_else(|_| Some(BlockInfo::new(block.clone())))
.unwrap_or_else(|| BlockInfo::new(block.clone()));
if block_info.signed_over {
@@ -333,7 +339,7 @@ impl Signer {
debug!("{self}: ACK: {ack:?}",);
block_info.signed_over = true;
self.signer_db
.insert_block(&block_info)
.insert_block(self.reward_cycle, &block_info)
.unwrap_or_else(|e| {
error!("{self}: Failed to insert block in DB: {e:?}");
});
@@ -367,7 +373,10 @@ impl Signer {
}
State::OperationInProgress => {
// We cannot execute the next command until the current one is finished...
debug!("{self}: Waiting for coordinator {coordinator_id:?} operation to finish...",);
debug!("{self}: Waiting for coordinator {coordinator_id:?} operation to finish. Coordinator state = {:?}", self.coordinator.state);
}
State::TenureCompleted => {
warn!("{self}: Tenure completed. This signer should have been cleaned up during refresh.",);
}
}
}
@@ -383,7 +392,10 @@ impl Signer {
BlockValidateResponse::Ok(block_validate_ok) => {
let signer_signature_hash = block_validate_ok.signer_signature_hash;
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
let mut block_info = match self.signer_db.block_lookup(&signer_signature_hash) {
let mut block_info = match self
.signer_db
.block_lookup(self.reward_cycle, &signer_signature_hash)
{
Ok(Some(block_info)) => block_info,
Ok(None) => {
// We have not seen this block before. Why are we getting a response for it?
@@ -398,7 +410,7 @@ impl Signer {
let is_valid = self.verify_block_transactions(stacks_client, &block_info.block);
block_info.valid = Some(is_valid);
self.signer_db
.insert_block(&block_info)
.insert_block(self.reward_cycle, &block_info)
.expect(&format!("{self}: Failed to insert block in DB"));
info!(
"{self}: Treating block validation for block {} as valid: {:?}",
@@ -409,7 +421,10 @@ impl Signer {
}
BlockValidateResponse::Reject(block_validate_reject) => {
let signer_signature_hash = block_validate_reject.signer_signature_hash;
let mut block_info = match self.signer_db.block_lookup(&signer_signature_hash) {
let mut block_info = match self
.signer_db
.block_lookup(self.reward_cycle, &signer_signature_hash)
{
Ok(Some(block_info)) => block_info,
Ok(None) => {
// We have not seen this block before. Why are we getting a response for it?
@@ -452,8 +467,9 @@ impl Signer {
{
// We are the coordinator. Trigger a signing round for this block
debug!(
"{self}: triggering a signing round over the block {}",
block_info.block.header.block_hash()
"{self}: attempt to trigger a signing round for block";
"signer_sighash" => %block_info.block.header.signer_signature_hash(),
"block_hash" => %block_info.block.header.block_hash(),
);
self.commands.push_back(Command::Sign {
block: block_info.block.clone(),
@@ -471,7 +487,7 @@ impl Signer {
}
}
self.signer_db
.insert_block(&block_info)
.insert_block(self.reward_cycle, &block_info)
.expect(&format!("{self}: Failed to insert block in DB"));
}
@@ -497,20 +513,59 @@ impl Signer {
}
/// Handle proposed blocks submitted by the miners to stackerdb
fn handle_proposed_blocks(&mut self, stacks_client: &StacksClient, blocks: &[NakamotoBlock]) {
for block in blocks {
// Store the block in our cache
self.signer_db
.insert_block(&BlockInfo::new(block.clone()))
.unwrap_or_else(|e| {
error!("{self}: Failed to insert block in DB: {e:?}");
});
// Submit the block for validation
stacks_client
.submit_block_for_validation(block.clone())
.unwrap_or_else(|e| {
warn!("{self}: Failed to submit block for validation: {e:?}");
});
fn handle_proposed_blocks(
&mut self,
stacks_client: &StacksClient,
proposals: &[BlockProposalSigners],
) {
for proposal in proposals {
if proposal.reward_cycle != self.reward_cycle {
debug!(
"{self}: Received proposal for block outside of my reward cycle, ignoring.";
"proposal_reward_cycle" => proposal.reward_cycle,
"proposal_burn_height" => proposal.burn_height,
);
continue;
}
let sig_hash = proposal.block.header.signer_signature_hash();
match self.signer_db.block_lookup(self.reward_cycle, &sig_hash) {
Ok(Some(block)) => {
debug!(
"{self}: Received proposal for block already known, ignoring new proposal.";
"signer_sighash" => %sig_hash,
"proposal_burn_height" => proposal.burn_height,
"vote" => ?block.vote.as_ref().map(|v| {
if v.rejected {
"REJECT"
} else {
"ACCEPT"
}
}),
"signed_over" => block.signed_over,
);
continue;
}
Ok(None) => {
// Store the block in our cache
self.signer_db
.insert_block(self.reward_cycle, &BlockInfo::new(proposal.block.clone()))
.unwrap_or_else(|e| {
error!("{self}: Failed to insert block in DB: {e:?}");
});
// Submit the block for validation
stacks_client
.submit_block_for_validation(proposal.block.clone())
.unwrap_or_else(|e| {
warn!("{self}: Failed to submit block for validation: {e:?}");
});
}
Err(e) => {
error!(
"{self}: Failed to lookup block in DB: {e:?}. Dropping proposal request."
);
continue;
}
}
}
}
@@ -568,13 +623,16 @@ impl Signer {
match self
.signer_db
.block_lookup(&block_vote.signer_signature_hash)
.block_lookup(self.reward_cycle, &block_vote.signer_signature_hash)
.expect(&format!("{self}: Failed to connect to DB"))
.map(|b| b.vote)
{
Some(Some(vote)) => {
// Overwrite with our agreed upon value in case another message won majority or the coordinator is trying to cheat...
debug!("{self}: set vote for {} to {vote:?}", block_vote.rejected);
debug!(
"{self}: Set vote (rejected = {}) to {vote:?}", block_vote.rejected;
"requested_sighash" => %block_vote.signer_signature_hash,
);
request.message = vote.serialize_to_vec();
true
}
@@ -582,7 +640,10 @@ impl Signer {
// We never agreed to sign this block. Reject it.
// This can happen if the coordinator received enough votes to sign yes
// or no on a block before we received validation from the stacks node.
debug!("{self}: Received a signature share request for a block we never agreed to sign. Ignore it.");
debug!(
"{self}: Received a signature share request for a block we never agreed to sign. Ignore it.";
"requested_sighash" => %block_vote.signer_signature_hash,
);
false
}
None => {
@@ -590,7 +651,8 @@ impl Signer {
// blocks we have seen a Nonce Request for (and subsequent validation)
// We are missing the context here necessary to make a decision. Reject the block
debug!(
"{self}: Received a signature share request from an unknown block. Reject it."
"{self}: Received a signature share request from an unknown block. Reject it.";
"requested_sighash" => %block_vote.signer_signature_hash,
);
false
}
@@ -615,7 +677,7 @@ impl Signer {
let signer_signature_hash = block.header.signer_signature_hash();
let mut block_info = match self
.signer_db
.block_lookup(&signer_signature_hash)
.block_lookup(self.reward_cycle, &signer_signature_hash)
.expect("Failed to connect to signer DB")
{
Some(block_info) => block_info,
@@ -623,7 +685,7 @@ impl Signer {
debug!("{self}: We have received a block sign request for a block we have not seen before. Cache the nonce request and submit the block for validation...");
let block_info = BlockInfo::new_with_request(block.clone(), nonce_request.clone());
self.signer_db
.insert_block(&block_info)
.insert_block(self.reward_cycle, &block_info)
.expect(&format!("{self}: Failed to insert block in DB"));
stacks_client
.submit_block_for_validation(block)
@@ -643,7 +705,7 @@ impl Signer {
self.determine_vote(&mut block_info, nonce_request);
self.signer_db
.insert_block(&block_info)
.insert_block(self.reward_cycle, &block_info)
.expect(&format!("{self}: Failed to insert block in DB"));
true
}
@@ -967,22 +1029,20 @@ impl Signer {
return;
};
// TODO: proper garbage collection...This is currently our only cleanup of blocks
self.signer_db
.remove_block(&block_vote.signer_signature_hash)
.expect(&format!("{self}: Failed to remove block from to signer DB"));
let block_submission = if block_vote.rejected {
// We signed a rejection message. Return a rejection message
BlockResponse::rejected(block_vote.signer_signature_hash, signature.clone()).into()
BlockResponse::rejected(block_vote.signer_signature_hash, signature.clone())
} else {
// we agreed to sign the block hash. Return an approval message
BlockResponse::accepted(block_vote.signer_signature_hash, signature.clone()).into()
BlockResponse::accepted(block_vote.signer_signature_hash, signature.clone())
};
// Submit signature result to miners to observe
debug!("{self}: submit block response {block_submission:?}");
if let Err(e) = self.stackerdb.send_message_with_retry(block_submission) {
info!("{self}: Submit block response: {block_submission}");
if let Err(e) = self
.stackerdb
.send_message_with_retry(block_submission.into())
{
warn!("{self}: Failed to send block submission to stacker-db: {e:?}");
}
}
@@ -1005,7 +1065,7 @@ impl Signer {
};
let Some(block_info) = self
.signer_db
.block_lookup(&block_vote.signer_signature_hash)
.block_lookup(self.reward_cycle, &block_vote.signer_signature_hash)
.expect(&format!("{self}: Failed to connect to signer DB"))
else {
debug!(

View File

@@ -18,6 +18,8 @@ use std::path::Path;
use blockstack_lib::util_lib::db::{query_row, sqlite_open, table_exists, Error as DBError};
use rusqlite::{Connection, Error as SqliteError, OpenFlags, NO_PARAMS};
use slog::slog_debug;
use stacks_common::debug;
use stacks_common::util::hash::Sha512Trunc256Sum;
use crate::signer::BlockInfo;
@@ -32,8 +34,10 @@ pub struct SignerDb {
const CREATE_BLOCKS_TABLE: &'static str = "
CREATE TABLE IF NOT EXISTS blocks (
signer_signature_hash TEXT PRIMARY KEY,
block_info TEXT NOT NULL
reward_cycle INTEGER NOT NULL,
signer_signature_hash TEXT NOT NULL,
block_info TEXT NOT NULL,
PRIMARY KEY (reward_cycle, signer_signature_hash)
)";
impl SignerDb {
@@ -68,11 +72,15 @@ impl SignerDb {
/// Fetch a block from the database using the block's
/// `signer_signature_hash`
pub fn block_lookup(&self, hash: &Sha512Trunc256Sum) -> Result<Option<BlockInfo>, DBError> {
pub fn block_lookup(
&self,
reward_cycle: u64,
hash: &Sha512Trunc256Sum,
) -> Result<Option<BlockInfo>, DBError> {
let result: Option<String> = query_row(
&self.db,
"SELECT block_info FROM blocks WHERE signer_signature_hash = ?",
&[format!("{}", hash)],
"SELECT block_info FROM blocks WHERE reward_cycle = ? AND signer_signature_hash = ?",
&[&reward_cycle.to_string(), &format!("{}", hash)],
)?;
if let Some(block_info) = result {
let block_info: BlockInfo =
@@ -85,14 +93,30 @@ impl SignerDb {
/// Insert a block into the database.
/// `hash` is the `signer_signature_hash` of the block.
pub fn insert_block(&mut self, block_info: &BlockInfo) -> Result<(), DBError> {
pub fn insert_block(
&mut self,
reward_cycle: u64,
block_info: &BlockInfo,
) -> Result<(), DBError> {
let block_json =
serde_json::to_string(&block_info).expect("Unable to serialize block info");
let hash = &block_info.signer_signature_hash();
let block_id = &block_info.block.block_id();
let signed_over = &block_info.signed_over;
debug!(
"Inserting block_info: reward_cycle = {reward_cycle}, sighash = {hash}, block_id = {block_id}, signed = {signed_over} vote = {:?}",
block_info.vote.as_ref().map(|v| {
if v.rejected {
"REJECT"
} else {
"ACCEPT"
}
})
);
self.db
.execute(
"INSERT OR REPLACE INTO blocks (signer_signature_hash, block_info) VALUES (?1, ?2)",
&[format!("{}", hash), block_json],
"INSERT OR REPLACE INTO blocks (reward_cycle, signer_signature_hash, block_info) VALUES (?1, ?2, ?3)",
&[reward_cycle.to_string(), format!("{}", hash), block_json],
)
.map_err(|e| {
return DBError::Other(format!(
@@ -104,10 +128,15 @@ impl SignerDb {
}
/// Remove a block
pub fn remove_block(&mut self, hash: &Sha512Trunc256Sum) -> Result<(), DBError> {
pub fn remove_block(
&mut self,
reward_cycle: u64,
hash: &Sha512Trunc256Sum,
) -> Result<(), DBError> {
debug!("Deleting block_info: sighash = {hash}");
self.db.execute(
"DELETE FROM blocks WHERE signer_signature_hash = ?",
&[format!("{}", hash)],
"DELETE FROM blocks WHERE reward_cycle = ? AND signer_signature_hash = ?",
&[reward_cycle.to_string(), format!("{}", hash)],
)?;
Ok(())
@@ -178,16 +207,23 @@ mod tests {
fn test_basic_signer_db_with_path(db_path: impl AsRef<Path>) {
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
let reward_cycle = 1;
let (block_info, block) = create_block();
db.insert_block(&block_info)
db.insert_block(reward_cycle, &block_info)
.expect("Unable to insert block into db");
let block_info = db
.block_lookup(&block.header.signer_signature_hash())
.block_lookup(reward_cycle, &block.header.signer_signature_hash())
.unwrap()
.expect("Unable to get block from db");
assert_eq!(BlockInfo::new(block.clone()), block_info);
// Test looking up a block from a different reward cycle
let block_info = db
.block_lookup(reward_cycle + 1, &block.header.signer_signature_hash())
.unwrap();
assert!(block_info.is_none());
}
#[test]
@@ -205,12 +241,13 @@ mod tests {
fn test_update_block() {
let db_path = tmp_db_path();
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
let reward_cycle = 42;
let (block_info, block) = create_block();
db.insert_block(&block_info)
db.insert_block(reward_cycle, &block_info)
.expect("Unable to insert block into db");
let block_info = db
.block_lookup(&block.header.signer_signature_hash())
.block_lookup(reward_cycle, &block.header.signer_signature_hash())
.unwrap()
.expect("Unable to get block from db");
@@ -231,11 +268,11 @@ mod tests {
rejected: false,
};
block_info.vote = Some(vote.clone());
db.insert_block(&block_info)
db.insert_block(reward_cycle, &block_info)
.expect("Unable to insert block into db");
let block_info = db
.block_lookup(&block.header.signer_signature_hash())
.block_lookup(reward_cycle, &block.header.signer_signature_hash())
.unwrap()
.expect("Unable to get block from db");

View File

@@ -524,11 +524,11 @@ impl NakamotoBlockBuilder {
/// Returns Some(chunk) if the given key corresponds to one of the expected miner slots
/// Returns None if not
/// Returns an error on signing or DB error
pub fn make_stackerdb_block_proposal(
pub fn make_stackerdb_block_proposal<T: StacksMessageCodec>(
sortdb: &SortitionDB,
tip: &BlockSnapshot,
stackerdbs: &StackerDBs,
block: &NakamotoBlock,
block: &T,
miner_privkey: &StacksPrivateKey,
miners_contract_id: &QualifiedContractIdentifier,
) -> Result<Option<StackerDBChunkData>, Error> {

View File

@@ -693,6 +693,12 @@ impl FromSql for ThresholdSignature {
}
}
impl fmt::Display for ThresholdSignature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
to_hex(&self.serialize_to_vec()).fmt(f)
}
}
impl ToSql for ThresholdSignature {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let bytes = self.serialize_to_vec();

View File

@@ -23,8 +23,8 @@ use clarity::vm::clarity::ClarityConnection;
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
use hashbrown::HashSet;
use libsigner::{
BlockResponse, RejectCode, SignerMessage, SignerSession, StackerDBSession, BLOCK_MSG_ID,
TRANSACTIONS_MSG_ID,
BlockProposalSigners, BlockResponse, RejectCode, SignerMessage, SignerSession,
StackerDBSession, BLOCK_MSG_ID, TRANSACTIONS_MSG_ID,
};
use stacks::burnchains::{Burnchain, BurnchainParameters};
use stacks::chainstate::burn::db::sortdb::SortitionDB;
@@ -39,7 +39,6 @@ use stacks::chainstate::stacks::{
TenureChangeCause, TenureChangePayload, ThresholdSignature, TransactionAnchorMode,
TransactionPayload, TransactionVersion,
};
use stacks::core::FIRST_BURNCHAIN_CONSENSUS_HASH;
use stacks::net::stackerdb::StackerDBs;
use stacks_common::codec::{read_next, StacksMessageCodec};
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
@@ -83,8 +82,6 @@ struct ParentTenureInfo {
struct ParentStacksBlockInfo {
/// Header metadata for the Stacks block we're going to build on top of
stacks_parent_header: StacksHeaderInfo,
/// the total amount burned in the sortition that selected the Stacks block parent
parent_block_total_burn: u64,
/// nonce to use for this new block's coinbase transaction
coinbase_nonce: u64,
parent_tenure: Option<ParentTenureInfo>,
@@ -193,37 +190,53 @@ impl BlockMinerThread {
.expect("FATAL: could not open sortition DB");
let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())
.expect("FATAL: could not retrieve chain tip");
let reward_cycle = self
.burnchain
.pox_constants
.block_height_to_reward_cycle(
self.burnchain.first_block_height,
self.burn_block.block_height,
)
.expect("FATAL: building on a burn block that is before the first burn block");
if let Some(new_block) = new_block {
match NakamotoBlockBuilder::make_stackerdb_block_proposal(
let proposal_msg = BlockProposalSigners {
block: new_block.clone(),
burn_height: self.burn_block.block_height,
reward_cycle,
};
let proposal = match NakamotoBlockBuilder::make_stackerdb_block_proposal(
&sort_db,
&tip,
&stackerdbs,
&new_block,
&proposal_msg,
&miner_privkey,
&miners_contract_id,
) {
Ok(Some(chunk)) => {
// Propose the block to the observing signers through the .miners stackerdb instance
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);
match miners_stackerdb.put_chunk(&chunk) {
Ok(ack) => {
info!("Proposed block to stackerdb: {ack:?}");
}
Err(e) => {
warn!("Failed to propose block to stackerdb {e:?}");
return;
}
}
}
Ok(Some(chunk)) => chunk,
Ok(None) => {
warn!("Failed to propose block to stackerdb: no slot available");
continue;
}
Err(e) => {
warn!("Failed to propose block to stackerdb: {e:?}");
continue;
}
};
// Propose the block to the observing signers through the .miners stackerdb instance
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);
match miners_stackerdb.put_chunk(&proposal) {
Ok(ack) => {
info!("Proposed block to stackerdb: {ack:?}");
}
Err(e) => {
warn!("Failed to propose block to stackerdb {e:?}");
return;
}
}
self.globals.counters.bump_naka_proposed_blocks();
if let Err(e) =
@@ -231,6 +244,14 @@ impl BlockMinerThread {
{
warn!("Error broadcasting block: {e:?}");
} else {
info!(
"Miner: Block signed by signer set and broadcasted";
"signer_sighash" => %new_block.header.signer_signature_hash(),
"block_hash" => %new_block.header.block_hash(),
"stacks_block_id" => %new_block.header.block_id(),
"block_height" => new_block.header.chain_length,
"consensus_hash" => %new_block.header.consensus_hash,
);
self.globals.coord().announce_new_stacks_block();
}
@@ -668,7 +689,6 @@ impl BlockMinerThread {
parent_tenure_blocks: 0,
}),
stacks_parent_header: chain_tip.metadata,
parent_block_total_burn: 0,
coinbase_nonce: 0,
});
};
@@ -839,15 +859,11 @@ impl BlockMinerThread {
block.header.miner_signature = miner_signature;
info!(
"Miner: Succeeded assembling {} block #{}: {}, with {} txs",
if parent_block_info.parent_block_total_burn == 0 {
"Genesis"
} else {
"Stacks"
},
"Miner: Assembled block #{} for signer set proposal: {}, with {} txs",
block.header.chain_length,
block.header.block_hash(),
block.txs.len(),
block.txs.len();
"signer_sighash" => %block.header.signer_signature_hash(),
);
// last chance -- confirm that the stacks tip is unchanged (since it could have taken long
@@ -895,26 +911,6 @@ impl ParentStacksBlockInfo {
.expect("Failed to look up block's parent snapshot")
.expect("Failed to look up block's parent snapshot");
let parent_sortition_id = &parent_snapshot.sortition_id;
let parent_block_total_burn =
if &stacks_tip_header.consensus_hash == &FIRST_BURNCHAIN_CONSENSUS_HASH {
0
} else {
let parent_burn_block =
SortitionDB::get_block_snapshot(burn_db.conn(), parent_sortition_id)
.expect("SortitionDB failure.")
.ok_or_else(|| {
error!(
"Failed to find block snapshot for the parent sortition";
"parent_sortition_id" => %parent_sortition_id
);
NakamotoNodeError::SnapshotNotFoundForChainTip
})?;
parent_burn_block.total_burn
};
// don't mine off of an old burnchain block
let burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(burn_db.conn())
.expect("FATAL: failed to query sortition DB for canonical burn chain tip");
@@ -1009,7 +1005,6 @@ impl ParentStacksBlockInfo {
Ok(ParentStacksBlockInfo {
stacks_parent_header: stacks_tip_header,
parent_block_total_burn,
coinbase_nonce,
parent_tenure: parent_tenure_info,
})

View File

@@ -447,7 +447,9 @@ impl SignerTest {
panic!("Received SignError {}", sign_error);
}
OperationResult::Dkg(point) => {
panic!("Received aggregate_group_key {point}");
// should not panic, because DKG may have just run for the
// next reward cycle.
info!("Received aggregate_group_key {point}");
}
}
}
@@ -963,8 +965,8 @@ fn stackerdb_sign() {
info!("------------------------- Test Setup -------------------------");
info!("Creating an invalid block to sign...");
let header = NakamotoBlockHeader {
info!("Creating invalid blocks to sign...");
let header1 = NakamotoBlockHeader {
version: 1,
chain_length: 2,
burn_spent: 3,
@@ -976,12 +978,12 @@ fn stackerdb_sign() {
signer_signature: ThresholdSignature::empty(),
signer_bitvec: BitVec::zeros(1).unwrap(),
};
let mut block = NakamotoBlock {
header,
let mut block1 = NakamotoBlock {
header: header1,
txs: vec![],
};
let tx_merkle_root = {
let txid_vecs = block
let tx_merkle_root1 = {
let txid_vecs = block1
.txs
.iter()
.map(|tx| tx.txid().as_bytes().to_vec())
@@ -989,14 +991,46 @@ fn stackerdb_sign() {
MerkleTree::<Sha512Trunc256Sum>::new(&txid_vecs).root()
};
block.header.tx_merkle_root = tx_merkle_root;
block1.header.tx_merkle_root = tx_merkle_root1;
let header2 = NakamotoBlockHeader {
version: 1,
chain_length: 3,
burn_spent: 4,
consensus_hash: ConsensusHash([0x05; 20]),
parent_block_id: StacksBlockId([0x06; 32]),
tx_merkle_root: Sha512Trunc256Sum([0x07; 32]),
state_index_root: TrieHash([0x08; 32]),
miner_signature: MessageSignature::empty(),
signer_signature: ThresholdSignature::empty(),
signer_bitvec: BitVec::zeros(1).unwrap(),
};
let mut block2 = NakamotoBlock {
header: header2,
txs: vec![],
};
let tx_merkle_root2 = {
let txid_vecs = block2
.txs
.iter()
.map(|tx| tx.txid().as_bytes().to_vec())
.collect();
MerkleTree::<Sha512Trunc256Sum>::new(&txid_vecs).root()
};
block2.header.tx_merkle_root = tx_merkle_root2;
// The block is invalid so the signers should return a signature across a rejection
let block_vote = NakamotoBlockVote {
signer_signature_hash: block.header.signer_signature_hash(),
let block1_vote = NakamotoBlockVote {
signer_signature_hash: block1.header.signer_signature_hash(),
rejected: true,
};
let msg = block_vote.serialize_to_vec();
let msg1 = block1_vote.serialize_to_vec();
let block2_vote = NakamotoBlockVote {
signer_signature_hash: block2.header.signer_signature_hash(),
rejected: true,
};
let msg2 = block2_vote.serialize_to_vec();
let timeout = Duration::from_secs(200);
let mut signer_test = SignerTest::new(10);
@@ -1010,7 +1044,7 @@ fn stackerdb_sign() {
let sign_command = RunLoopCommand {
reward_cycle,
command: SignerCommand::Sign {
block: block.clone(),
block: block1,
is_taproot: false,
merkle_root: None,
},
@@ -1018,7 +1052,7 @@ fn stackerdb_sign() {
let sign_taproot_command = RunLoopCommand {
reward_cycle,
command: SignerCommand::Sign {
block: block.clone(),
block: block2,
is_taproot: true,
merkle_root: None,
},
@@ -1035,12 +1069,12 @@ fn stackerdb_sign() {
let schnorr_proofs = signer_test.wait_for_taproot_signatures(timeout);
for frost_signature in frost_signatures {
assert!(frost_signature.verify(&key, &msg));
assert!(frost_signature.verify(&key, &msg1));
}
for schnorr_proof in schnorr_proofs {
let tweaked_key = tweaked_public_key(&key, None);
assert!(
schnorr_proof.verify(&tweaked_key.x(), &msg),
schnorr_proof.verify(&tweaked_key.x(), &msg2),
"Schnorr proof verification failed"
);
}