Merge remote-tracking branch 'origin/next' into feat/stacker-bitvec

This commit is contained in:
Aaron Blankstein
2024-01-29 10:44:40 -06:00
11 changed files with 123 additions and 132 deletions

View File

@@ -12,7 +12,7 @@ COPY . .
RUN apt-get update && apt-get install -y git libclang-dev
# Run all the build steps in ramdisk in an attempt to speed things up
RUN target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \
RUN --mount=type=tmpfs,target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \
&& cd ${BUILD_DIR} \
&& rustup target add ${TARGET} \
&& rustup component add rustfmt \
@@ -21,5 +21,5 @@ RUN target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \
&& cp -R ${BUILD_DIR}/target/${TARGET}/release/. /out
FROM --platform=${TARGETPLATFORM} debian:bookworm
COPY --from=build /out/stacks-node /bin/
COPY --from=build /out/stacks-node /out/stacks-signer /bin/
CMD ["stacks-node", "mainnet"]

View File

@@ -23,6 +23,7 @@ use std::sync::Arc;
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::chainstate::stacks::boot::{MINERS_NAME, SIGNERS_NAME};
use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent;
use blockstack_lib::chainstate::stacks::{StacksTransaction, ThresholdSignature};
use blockstack_lib::net::api::postblock_proposal::{
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
};
@@ -32,9 +33,11 @@ use serde::{Deserialize, Serialize};
use stacks_common::codec::{
read_next, read_next_at_most, write_next, Error as CodecError, StacksMessageCodec,
};
use stacks_common::util::hash::Sha512Trunc256Sum;
use tiny_http::{
Method as HttpMethod, Request as HttpRequest, Response as HttpResponse, Server as HttpServer,
};
use wsts::common::Signature;
use wsts::net::{Message, Packet};
use crate::http::{decode_http_body, decode_http_request};
@@ -73,11 +76,26 @@ pub enum SignerMessage {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum BlockResponse {
/// The Nakamoto block was accepted and therefore signed
Accepted(NakamotoBlock),
Accepted((Sha512Trunc256Sum, ThresholdSignature)),
/// The Nakamoto block was rejected and therefore not signed
Rejected(BlockRejection),
}
impl BlockResponse {
/// Create a new accepted BlockResponse for the provided block signer signature hash and signature
pub fn accepted(hash: Sha512Trunc256Sum, sig: Signature) -> Self {
Self::Accepted((hash, ThresholdSignature(sig)))
}
/// Create a new rejected BlockResponse for the provided block signer signature hash and signature
pub fn rejected(hash: Sha512Trunc256Sum, sig: Signature) -> Self {
Self::Rejected(BlockRejection::new(
hash,
RejectCode::SignedRejection(ThresholdSignature(sig)),
))
}
}
/// A rejection response from a signer for a proposed block
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockRejection {
@@ -85,17 +103,17 @@ pub struct BlockRejection {
pub reason: String,
/// The reason code for the rejection
pub reason_code: RejectCode,
/// The block that was rejected
pub block: NakamotoBlock,
/// The signer signature hash of the block that was rejected
pub signer_signature_hash: Sha512Trunc256Sum,
}
impl BlockRejection {
/// Create a new BlockRejection for the provided block and reason code
pub fn new(block: NakamotoBlock, reason_code: RejectCode) -> Self {
pub fn new(signer_signature_hash: Sha512Trunc256Sum, reason_code: RejectCode) -> Self {
Self {
reason: reason_code.to_string(),
reason_code,
block,
signer_signature_hash,
}
}
}
@@ -105,7 +123,7 @@ impl From<BlockValidateReject> for BlockRejection {
Self {
reason: reject.reason,
reason_code: RejectCode::ValidationFailed(reject.reason_code),
block: reject.block,
signer_signature_hash: reject.signer_signature_hash,
}
}
}
@@ -117,9 +135,7 @@ pub enum RejectCode {
/// RPC endpoint Validation failed
ValidationFailed(ValidateRejectCode),
/// Signers signed a block rejection
SignedRejection,
/// Invalid signature hash
InvalidSignatureHash,
SignedRejection(ThresholdSignature),
/// Insufficient signers agreed to sign the block
InsufficientSigners(Vec<u32>),
}
@@ -128,10 +144,9 @@ impl std::fmt::Display for RejectCode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RejectCode::ValidationFailed(code) => write!(f, "Validation failed: {:?}", code),
RejectCode::SignedRejection => {
write!(f, "A threshold number of signers rejected the block.")
RejectCode::SignedRejection(sig) => {
write!(f, "A threshold number of signers rejected the block with the following signature: {:?}.", sig)
}
RejectCode::InvalidSignatureHash => write!(f, "The signature hash was invalid."),
RejectCode::InsufficientSigners(malicious_signers) => write!(
f,
"Insufficient signers agreed to sign the block. The following signers are malicious: {:?}",

View File

@@ -340,10 +340,10 @@ impl Config {
#[cfg(test)]
mod tests {
use super::Network;
use super::{Config, RawConfigFile};
use blockstack_lib::util_lib::boot::boot_code_id;
use super::{Config, Network, RawConfigFile};
fn create_raw_config(overrides: impl FnOnce(&mut RawConfigFile)) -> RawConfigFile {
let mut config = RawConfigFile {
node_host: "127.0.0.1:20443".to_string(),

View File

@@ -19,7 +19,6 @@ use std::time::Duration;
use blockstack_lib::burnchains::Txid;
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::chainstate::stacks::ThresholdSignature;
use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse;
use hashbrown::{HashMap, HashSet};
use libsigner::{
@@ -186,13 +185,10 @@ impl<C: Coordinator> RunLoop<C> {
is_taproot,
merkle_root,
} => {
let Ok(hash) = block.header.signer_signature_hash() else {
error!("Failed to sign block. Invalid signature hash.");
return false;
};
let signer_signature_hash = block.header.signer_signature_hash();
let block_info = self
.blocks
.entry(hash)
.entry(signer_signature_hash)
.or_insert_with(|| BlockInfo::new(block.clone()));
if block_info.signing_round {
debug!("Received a sign command for a block we are already signing over. Ignore it.");
@@ -256,29 +252,29 @@ impl<C: Coordinator> RunLoop<C> {
res: Sender<Vec<OperationResult>>,
) {
let transactions = &self.transactions;
let (block_info, hash) = match block_validate_response {
let block_info = match block_validate_response {
BlockValidateResponse::Ok(block_validate_ok) => {
let Ok(hash) = block_validate_ok.block.header.signer_signature_hash() else {
self.broadcast_signature_hash_rejection(block_validate_ok.block);
let Some(block_info) = self
.blocks
.get_mut(&block_validate_ok.signer_signature_hash)
else {
// We have not seen this block before. Why are we getting a response for it?
debug!("Received a block validate response for a block we have not seen before. Ignoring...");
return;
};
let block_info = self
.blocks
.entry(hash)
.or_insert(BlockInfo::new(block_validate_ok.block.clone()));
block_info.valid = Some(true);
(block_info, hash)
block_info
}
BlockValidateResponse::Reject(block_validate_reject) => {
// There is no point in triggering a sign round for this block if validation failed from the stacks node
let Ok(hash) = block_validate_reject.block.header.signer_signature_hash() else {
self.broadcast_signature_hash_rejection(block_validate_reject.block);
let Some(block_info) = self
.blocks
.get_mut(&block_validate_reject.signer_signature_hash)
else {
// We have not seen this block before. Why are we getting a response for it?
debug!("Received a block validate response for a block we have not seen before. Ignoring...");
return;
};
let block_info = self
.blocks
.entry(hash)
.or_insert(BlockInfo::new(block_validate_reject.block.clone()));
block_info.valid = Some(false);
// Submit a rejection response to the .signers contract for miners
// to observe so they know to send another block and to prove signers are doing work);
@@ -288,14 +284,14 @@ impl<C: Coordinator> RunLoop<C> {
) {
warn!("Failed to send block rejection to stacker-db: {:?}", e);
}
(block_info, hash)
block_info
}
};
if let Some(mut request) = block_info.nonce_request.take() {
debug!("Received a block validate response from the stacks node for a block we already received a nonce request for. Responding to the nonce request...");
// We have an associated nonce request. Respond to it
Self::determine_vote(block_info, &mut request, transactions, hash);
Self::determine_vote(block_info, &mut request, transactions);
// Send the nonce request through with our vote
let packet = Packet {
msg: Message::NonceRequest(request),
@@ -345,12 +341,11 @@ impl<C: Coordinator> RunLoop<C> {
/// Handle proposed blocks submitted by the miners to stackerdb
fn handle_proposed_blocks(&mut self, blocks: Vec<NakamotoBlock>) {
for block in blocks {
let Ok(hash) = block.header.signer_signature_hash() else {
self.broadcast_signature_hash_rejection(block);
continue;
};
// Store the block in our cache
self.blocks.insert(hash, BlockInfo::new(block.clone()));
self.blocks.insert(
block.header.signer_signature_hash(),
BlockInfo::new(block.clone()),
);
// Submit the block for validation
self.stacks_client
.submit_block_for_validation(block)
@@ -447,19 +442,14 @@ impl<C: Coordinator> RunLoop<C> {
debug!("Received a nonce request for an unknown message stream. Reject it.");
return false;
};
let Ok(hash) = block.header.signer_signature_hash() else {
debug!(
"Received a nonce request for a block with an invalid signature hash. Reject it"
);
return false;
};
let transactions = &self.transactions;
let Some(block_info) = self.blocks.get_mut(&hash) else {
let signer_signature_hash = block.header.signer_signature_hash();
let Some(block_info) = self.blocks.get_mut(&signer_signature_hash) else {
// We have not seen this block before. Cache it. Send a RPC to the stacks node to validate it.
debug!("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...");
// Store the block in our cache
self.blocks.insert(
hash,
signer_signature_hash,
BlockInfo::new_with_request(block.clone(), request.clone()),
);
self.stacks_client
@@ -475,7 +465,7 @@ impl<C: Coordinator> RunLoop<C> {
block_info.nonce_request = Some(request.clone());
return false;
}
Self::determine_vote(block_info, request, transactions, hash);
Self::determine_vote(block_info, request, transactions);
true
}
@@ -484,9 +474,8 @@ impl<C: Coordinator> RunLoop<C> {
block_info: &mut BlockInfo,
nonce_request: &mut NonceRequest,
transactions: &[Txid],
hash: Sha512Trunc256Sum,
) {
let mut vote_bytes = hash.0.to_vec();
let mut vote_bytes = block_info.block.header.signer_signature_hash().0.to_vec();
// Validate the block contents
if !block_info.valid.unwrap_or(false)
|| !transactions
@@ -574,35 +563,34 @@ impl<C: Coordinator> RunLoop<C> {
};
let message = self.coordinator.get_message();
// This jankiness is because a coordinator could have signed a rejection we need to find the underlying block hash
let block_hash_bytes = if message.len() > 32 {
let signer_signature_hash_bytes = if message.len() > 32 {
&message[..32]
} else {
&message
};
let Some(block_hash) = Sha512Trunc256Sum::from_bytes(block_hash_bytes) else {
let Some(signer_signature_hash) =
Sha512Trunc256Sum::from_bytes(signer_signature_hash_bytes)
else {
debug!("Received a signature result for a signature over a non-block. Nothing to broadcast.");
return;
};
let Some(block_info) = self.blocks.remove(&block_hash) else {
debug!("Received a signature result for a block we have not seen before. Ignoring...");
return;
};
// TODO: proper garbage collection...This is currently our only cleanup of blocks
self.blocks.remove(&signer_signature_hash);
// This signature is no longer valid. Do not broadcast it.
if !signature.verify(aggregate_public_key, &message) {
warn!("Received an invalid signature result across the block. Do not broadcast it.");
// TODO: should we reinsert it and trigger a sign round across the block again?
return;
}
// Update the block signature hash with what the signers produced.
let mut block = block_info.block;
block.header.signer_signature = ThresholdSignature(signature.clone());
let block_submission = if message == block_hash.0.to_vec() {
let block_submission = if message == signer_signature_hash.0.to_vec() {
// we agreed to sign the block hash. Return an approval message
BlockResponse::Accepted(block).into()
BlockResponse::accepted(signer_signature_hash, signature.clone()).into()
} else {
// We signed a rejection message. Return a rejection message
BlockRejection::new(block, RejectCode::SignedRejection).into()
BlockResponse::rejected(signer_signature_hash, signature.clone()).into()
};
// Submit signature result to miners to observe
@@ -627,16 +615,16 @@ impl<C: Coordinator> RunLoop<C> {
let block = read_next::<NakamotoBlock, _>(&mut &message[..]).ok().unwrap_or({
// This is not a block so maybe its across its hash
// This jankiness is because a coordinator could have signed a rejection we need to find the underlying block hash
let block_hash_bytes = if message.len() > 32 {
let signer_signature_hash_bytes = if message.len() > 32 {
&message[..32]
} else {
&message
};
let Some(block_hash) = Sha512Trunc256Sum::from_bytes(block_hash_bytes) else {
let Some(signer_signature_hash) = Sha512Trunc256Sum::from_bytes(signer_signature_hash_bytes) else {
debug!("Received a signature result for a signature over a non-block. Nothing to broadcast.");
return;
};
let Some(block_info) = self.blocks.remove(&block_hash) else {
let Some(block_info) = self.blocks.remove(&signer_signature_hash) else {
debug!("Received a signature result for a block we have not seen before. Ignoring...");
return;
};
@@ -644,7 +632,7 @@ impl<C: Coordinator> RunLoop<C> {
});
// We don't have enough signers to sign the block. Broadcast a rejection
let block_rejection = BlockRejection::new(
block,
block.header.signer_signature_hash(),
RejectCode::InsufficientSigners(malicious_signers.clone()),
);
// Submit signature result to miners to observe
@@ -696,19 +684,6 @@ impl<C: Coordinator> RunLoop<C> {
}
}
}
/// Broadcast a block rejection due to an invalid block signature hash
fn broadcast_signature_hash_rejection(&mut self, block: NakamotoBlock) {
debug!("Broadcasting a block rejection due to a block with an invalid signature hash...");
let block_rejection = BlockRejection::new(block, RejectCode::InvalidSignatureHash);
// Submit signature result to miners to observe
if let Err(e) = self
.stackerdb
.send_message_with_retry(self.signing_round.signer_id, block_rejection.into())
{
warn!("Failed to send block submission to stacker-db: {:?}", e);
}
}
}
impl From<&Config> for RunLoop<FireCoordinator<v2::Aggregator>> {

View File

@@ -403,7 +403,21 @@ impl StacksMessageCodec for NakamotoBlockHeader {
impl NakamotoBlockHeader {
/// Calculate the message digest for miners to sign.
/// This includes all fields _except_ the signatures.
pub fn miner_signature_hash(&self) -> Result<Sha512Trunc256Sum, CodecError> {
pub fn miner_signature_hash(&self) -> Sha512Trunc256Sum {
self.miner_signature_hash_inner()
.expect("BUG: failed to calculate miner signature hash")
}
/// Calculate the message digest for signers to sign.
/// This includes all fields _except_ the signer signature.
pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum {
self.signer_signature_hash_inner()
.expect("BUG: failed to calculate signer signature hash")
}
/// Inner calculation of the message digest for miners to sign.
/// This includes all fields _except_ the signatures.
fn miner_signature_hash_inner(&self) -> Result<Sha512Trunc256Sum, CodecError> {
let mut hasher = Sha512_256::new();
let fd = &mut hasher;
write_next(fd, &self.version)?;
@@ -416,9 +430,9 @@ impl NakamotoBlockHeader {
Ok(Sha512Trunc256Sum::from_hasher(hasher))
}
/// Calculate the message digest for stackers to sign.
/// Inner calculation of the message digest for stackers to sign.
/// This includes all fields _except_ the stacker signature.
pub fn signer_signature_hash(&self) -> Result<Sha512Trunc256Sum, CodecError> {
fn signer_signature_hash_inner(&self) -> Result<Sha512Trunc256Sum, CodecError> {
let mut hasher = Sha512_256::new();
let fd = &mut hasher;
write_next(fd, &self.version)?;
@@ -434,7 +448,7 @@ impl NakamotoBlockHeader {
}
pub fn recover_miner_pk(&self) -> Option<StacksPublicKey> {
let signed_hash = self.miner_signature_hash().ok()?;
let signed_hash = self.miner_signature_hash();
let recovered_pk =
StacksPublicKey::recover_to_pubkey(signed_hash.bits(), &self.miner_signature).ok()?;
@@ -456,7 +470,7 @@ impl NakamotoBlockHeader {
/// Sign the block header by the miner
pub fn sign_miner(&mut self, privk: &StacksPrivateKey) -> Result<(), ChainstateError> {
let sighash = self.miner_signature_hash()?.0;
let sighash = self.miner_signature_hash().0;
let sig = privk
.sign(&sighash)
.map_err(|se| net_error::SigningError(se.to_string()))?;
@@ -1727,7 +1741,7 @@ impl NakamotoChainState {
if !db_handle.expects_signer_signature(
&block.header.consensus_hash,
schnorr_signature,
&block.header.signer_signature_hash()?.0,
&block.header.signer_signature_hash().0,
aggregate_public_key,
)? {
let msg = format!("Received block, but the stacker signature does not match the active stacking cycle");

View File

@@ -155,11 +155,7 @@ impl Default for TestSigners {
impl TestSigners {
pub fn sign_nakamoto_block(&mut self, block: &mut NakamotoBlock) {
let mut rng = rand_core::OsRng;
let msg = block
.header
.signer_signature_hash()
.expect("Failed to determine the block header signature hash for signers.")
.0;
let msg = block.header.signer_signature_hash().0;
let (nonces, sig_shares, key_ids) =
wsts::v2::test_helpers::sign(msg.as_slice(), &mut self.signer_parties, &mut rng);

View File

@@ -31,7 +31,7 @@ use stacks_common::types::chainstate::{
use stacks_common::types::net::PeerHost;
use stacks_common::types::StacksPublicKeyBuffer;
use stacks_common::util::get_epoch_time_ms;
use stacks_common::util::hash::{hex_bytes, to_hex, Hash160, Sha256Sum};
use stacks_common::util::hash::{hex_bytes, to_hex, Hash160, Sha256Sum, Sha512Trunc256Sum};
use stacks_common::util::retry::BoundReader;
use crate::burnchains::affirmation::AffirmationMap;
@@ -90,8 +90,7 @@ fn hex_deser_block<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NakamotoBlo
/// that the stacks-node thinks should be rejected.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockValidateReject {
#[serde(serialize_with = "hex_ser_block", deserialize_with = "hex_deser_block")]
pub block: NakamotoBlock,
pub signer_signature_hash: Sha512Trunc256Sum,
pub reason: String,
pub reason_code: ValidateRejectCode,
}
@@ -119,8 +118,7 @@ where
/// that the stacks-node thinks is acceptable.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockValidateOk {
#[serde(serialize_with = "hex_ser_block", deserialize_with = "hex_deser_block")]
pub block: NakamotoBlock,
pub signer_signature_hash: Sha512Trunc256Sum,
pub cost: ExecutionCost,
pub size: u64,
}
@@ -166,7 +164,7 @@ impl NakamotoBlockProposal {
let result =
self.validate(&sortdb, &mut chainstate)
.map_err(|reason| BlockValidateReject {
block: self.block.clone(),
signer_signature_hash: self.block.header.signer_signature_hash(),
reason_code: reason.reason_code,
reason: reason.reason,
});
@@ -326,7 +324,11 @@ impl NakamotoBlockProposal {
})
);
Ok(BlockValidateOk { block, cost, size })
Ok(BlockValidateOk {
signer_signature_hash: block.header.signer_signature_hash(),
cost,
size,
})
}
}

View File

@@ -979,7 +979,7 @@ impl MockamotoNode {
let miner_signature = self
.miner_key
.sign(block.header.miner_signature_hash().unwrap().as_bytes())
.sign(block.header.miner_signature_hash().as_bytes())
.unwrap();
block.header.miner_signature = miner_signature;

View File

@@ -66,11 +66,7 @@ impl SelfSigner {
pub fn sign_nakamoto_block(&mut self, block: &mut NakamotoBlock) {
let mut rng = rand::rngs::OsRng::default();
let msg = block
.header
.signer_signature_hash()
.expect("Failed to determine the block header signature hash for signers.")
.0;
let msg = block.header.signer_signature_hash().0;
let (nonces, sig_shares, key_ids) =
wsts::v2::test_helpers::sign(msg.as_slice(), &mut self.signer_parties, &mut rng);

View File

@@ -562,13 +562,7 @@ impl BlockMinerThread {
let mining_key = self.keychain.get_nakamoto_sk();
let miner_signature = mining_key
.sign(
block
.header
.miner_signature_hash()
.map_err(|_| NakamotoNodeError::SigningError("Could not create sighash"))?
.as_bytes(),
)
.sign(block.header.miner_signature_hash().as_bytes())
.map_err(NakamotoNodeError::SigningError)?;
block.header.miner_signature = miner_signature;

View File

@@ -384,12 +384,7 @@ fn stackerdb_dkg_sign() {
block.header.tx_merkle_root = tx_merkle_root;
// The block is invalid so the signers should return a signature across its hash + b'n'
let mut msg = block
.header
.signer_signature_hash()
.expect("Failed to get signature hash")
.0
.to_vec();
let mut msg = block.header.signer_signature_hash().0.to_vec();
msg.push(b'n');
let signer_test = SignerTest::new(10, 400);
@@ -642,16 +637,16 @@ fn stackerdb_block_proposal() {
thread::sleep(Duration::from_secs(1));
}
let validate_responses = test_observer::get_proposal_responses();
let mut proposed_block = match validate_responses.first().expect("No block proposal") {
BlockValidateResponse::Ok(block_validated) => block_validated.block.clone(),
_ => panic!("Unexpected response"),
};
let signature_hash = proposed_block
.header
.signer_signature_hash()
.expect("Unable to retrieve signature hash from proposed block");
let proposed_signer_signature_hash =
match validate_responses.first().expect("No block proposal") {
BlockValidateResponse::Ok(block_validated) => block_validated.signer_signature_hash,
_ => panic!("Unexpected response"),
};
assert!(
signature.verify(&aggregate_public_key, signature_hash.0.as_slice()),
signature.verify(
&aggregate_public_key,
proposed_signer_signature_hash.0.as_slice()
),
"Signature verification failed"
);
// Verify that the signers broadcasted a signed NakamotoBlock back to the .signers contract
@@ -680,9 +675,13 @@ fn stackerdb_block_proposal() {
}
let chunk = chunk.unwrap();
let signer_message = bincode::deserialize::<SignerMessage>(&chunk).unwrap();
if let SignerMessage::BlockResponse(BlockResponse::Accepted(block)) = signer_message {
proposed_block.header.signer_signature = ThresholdSignature(signature);
assert_eq!(block, proposed_block);
if let SignerMessage::BlockResponse(BlockResponse::Accepted((
block_signer_signature_hash,
block_signature,
))) = signer_message
{
assert_eq!(block_signer_signature_hash, proposed_signer_signature_hash);
assert_eq!(block_signature, ThresholdSignature(signature));
} else {
panic!("Received unexpected message");
}