diff --git a/Cargo.lock b/Cargo.lock index 393e918a7..10f8f4cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,7 @@ dependencies = [ name = "libsigner" version = "0.0.1" dependencies = [ + "bincode", "clarity", "libc", "libstackerdb", diff --git a/libsigner/Cargo.toml b/libsigner/Cargo.toml index ee7338ea1..d115a7475 100644 --- a/libsigner/Cargo.toml +++ b/libsigner/Cargo.toml @@ -16,6 +16,7 @@ name = "libsigner" path = "./src/libsigner.rs" [dependencies] +bincode = "1.3.3" clarity = { path = "../clarity" } libc = "0.2" libstackerdb = { path = "../libstackerdb" } diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index dde39a3f8..4e914e13d 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -21,9 +21,11 @@ use std::sync::mpsc::Sender; use std::sync::Arc; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; +use blockstack_lib::chainstate::stacks::boot::{MINERS_NAME, SIGNERS_NAME}; use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; -use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; +use blockstack_lib::net::api::postblock_proposal::{ + BlockValidateReject, BlockValidateResponse, ValidateRejectCode, +}; use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::types::QualifiedContractIdentifier; use serde::{Deserialize, Serialize}; @@ -38,13 +40,155 @@ use wsts::net::{Message, Packet}; use crate::http::{decode_http_body, decode_http_request}; use crate::EventError; +/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future +/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 +/// Is equal to the number of message types +pub const SIGNER_SLOTS_PER_USER: u32 = 11; + +// The slot IDS for each message type +const DKG_BEGIN_SLOT_ID: u32 = 0; +const DKG_PRIVATE_BEGIN_SLOT_ID: u32 = 1; +const DKG_END_BEGIN_SLOT_ID: u32 = 2; +const DKG_END_SLOT_ID: u32 = 3; +const DKG_PUBLIC_SHARES_SLOT_ID: u32 = 4; +const DKG_PRIVATE_SHARES_SLOT_ID: u32 = 5; +const NONCE_REQUEST_SLOT_ID: u32 = 6; +const NONCE_RESPONSE_SLOT_ID: u32 = 7; +const SIGNATURE_SHARE_REQUEST_SLOT_ID: u32 = 8; +const SIGNATURE_SHARE_RESPONSE_SLOT_ID: u32 = 9; +/// The slot ID for the block response for miners to observe +pub const BLOCK_SLOT_ID: u32 = 10; + +/// The messages being sent through the stacker db contracts +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum SignerMessage { + /// The signed/validated Nakamoto block for miners to observe + BlockResponse(BlockResponse), + /// DKG and Signing round data for other signers to observe + Packet(Packet), +} + +/// The response that a signer sends back to observing miners +/// either accepting or rejecting a Nakamoto block with the corresponding reason +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum BlockResponse { + /// The Nakamoto block was accepted and therefore signed + Accepted(NakamotoBlock), + /// The Nakamoto block was rejected and therefore not signed + Rejected(BlockRejection), +} + +/// A rejection response from a signer for a proposed block +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BlockRejection { + /// The reason for the rejection + pub reason: String, + /// The reason code for the rejection + pub reason_code: RejectCode, + /// The block that was rejected + pub block: NakamotoBlock, +} + +impl BlockRejection { + /// Create a new BlockRejection for the provided block and reason code + pub fn new(block: NakamotoBlock, reason_code: RejectCode) -> Self { + Self { + reason: reason_code.to_string(), + reason_code, + block, + } + } +} + +impl From for BlockRejection { + fn from(reject: BlockValidateReject) -> Self { + Self { + reason: reject.reason, + reason_code: RejectCode::ValidationFailed(reject.reason_code), + block: reject.block, + } + } +} + +/// This enum is used to supply a `reason_code` for block rejections +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[repr(u8)] +pub enum RejectCode { + /// RPC endpoint Validation failed + ValidationFailed(ValidateRejectCode), + /// Signers signed a block rejection + SignedRejection, + /// Invalid signature hash + InvalidSignatureHash, +} + +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::InvalidSignatureHash => write!(f, "The signature hash was invalid."), + } + } +} + +impl From for SignerMessage { + fn from(packet: Packet) -> Self { + Self::Packet(packet) + } +} + +impl From for SignerMessage { + fn from(block_response: BlockResponse) -> Self { + Self::BlockResponse(block_response) + } +} + +impl From for SignerMessage { + fn from(block_rejection: BlockRejection) -> Self { + Self::BlockResponse(BlockResponse::Rejected(block_rejection)) + } +} + +impl From for SignerMessage { + fn from(rejection: BlockValidateReject) -> Self { + Self::BlockResponse(BlockResponse::Rejected(rejection.into())) + } +} + +impl SignerMessage { + /// Helper function to determine the slot ID for the provided stacker-db writer id + pub fn slot_id(&self, id: u32) -> u32 { + let slot_id = match self { + Self::Packet(packet) => match packet.msg { + Message::DkgBegin(_) => DKG_BEGIN_SLOT_ID, + Message::DkgPrivateBegin(_) => DKG_PRIVATE_BEGIN_SLOT_ID, + Message::DkgEndBegin(_) => DKG_END_BEGIN_SLOT_ID, + Message::DkgEnd(_) => DKG_END_SLOT_ID, + Message::DkgPublicShares(_) => DKG_PUBLIC_SHARES_SLOT_ID, + Message::DkgPrivateShares(_) => DKG_PRIVATE_SHARES_SLOT_ID, + Message::NonceRequest(_) => NONCE_REQUEST_SLOT_ID, + Message::NonceResponse(_) => NONCE_RESPONSE_SLOT_ID, + Message::SignatureShareRequest(_) => SIGNATURE_SHARE_REQUEST_SLOT_ID, + Message::SignatureShareResponse(_) => SIGNATURE_SHARE_RESPONSE_SLOT_ID, + }, + Self::BlockResponse(_) => BLOCK_SLOT_ID, + }; + SIGNER_SLOTS_PER_USER * id + slot_id + } +} + /// Event enum for newly-arrived signer subscribed events #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum SignerEvent { - /// A new stackerDB chunk was received - StackerDB(StackerDBChunksEvent), - /// A new block proposal was received - BlockProposal(BlockValidateResponse), + /// The miner proposed blocks for signers to observe and sign + ProposedBlocks(Vec), + /// The signer messages for other signers and miners to observe + SignerMessages(Vec), + /// A new block proposal validation response from the node + BlockValidationResponse(BlockValidateResponse), } /// Trait to implement a stop-signaler for the event receiver thread. @@ -55,7 +199,7 @@ pub trait EventStopSignaler { fn send(&mut self); } -/// Trait to implement to handle StackerDB and BlockProposal events sent by the Stacks node +/// Trait to implement to handle signer specific events sent by the Stacks node pub trait EventReceiver { /// The implementation of ST will ensure that a call to ST::send() will cause /// the call to `is_stopped()` below to return true. @@ -120,25 +264,31 @@ pub struct SignerEventReceiver { out_channels: Vec>, /// inter-thread stop variable -- if set to true, then the `main_loop` will exit stop_signal: Arc, + /// Whether the receiver is running on mainnet + is_mainnet: bool, } impl SignerEventReceiver { /// Make a new Signer event receiver, and return both the receiver and the read end of a /// channel into which node-received data can be obtained. - pub fn new(contract_ids: Vec) -> SignerEventReceiver { + pub fn new( + contract_ids: Vec, + is_mainnet: bool, + ) -> SignerEventReceiver { SignerEventReceiver { stackerdb_contract_ids: contract_ids, http_server: None, local_addr: None, out_channels: vec![], stop_signal: Arc::new(AtomicBool::new(false)), + is_mainnet, } } /// Do something with the socket pub fn with_server(&mut self, todo: F) -> Result where - F: FnOnce(&SignerEventReceiver, &mut HttpServer, &[QualifiedContractIdentifier]) -> R, + F: FnOnce(&SignerEventReceiver, &mut HttpServer, bool) -> R, { let mut server = if let Some(s) = self.http_server.take() { s @@ -146,7 +296,7 @@ impl SignerEventReceiver { return Err(EventError::NotBound); }; - let res = todo(self, &mut server, &self.stackerdb_contract_ids); + let res = todo(self, &mut server, self.is_mainnet); self.http_server = Some(server); Ok(res) @@ -203,14 +353,12 @@ impl EventReceiver for SignerEventReceiver { /// Errors are recoverable -- the caller should call this method again even if it returns an /// error. fn next_event(&mut self) -> Result { - self.with_server(|event_receiver, http_server, contract_ids| { - let mut request = http_server.recv()?; - + self.with_server(|event_receiver, http_server, is_mainnet| { // were we asked to terminate? if event_receiver.is_stopped() { return Err(EventError::Terminated); } - + let request = http_server.recv()?; if request.method() != &HttpMethod::Post { return Err(EventError::MalformedRequest(format!( "Unrecognized method '{}'", @@ -218,71 +366,9 @@ impl EventReceiver for SignerEventReceiver { ))); } if request.url() == "/stackerdb_chunks" { - debug!("Got stackerdb_chunks event"); - let mut body = String::new(); - if let Err(e) = request - .as_reader() - .read_to_string(&mut body) { - error!("Failed to read body: {:?}", &e); - - request - .respond(HttpResponse::empty(200u16)) - .expect("response failed"); - return Err(EventError::MalformedRequest(format!( - "Failed to read body: {:?}", - &e - ))); - } - - let event: StackerDBChunksEvent = - serde_json::from_slice(body.as_bytes()).map_err(|e| { - EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)) - })?; - - if !contract_ids.contains(&event.contract_id) { - info!( - "[{:?}] next_event got event from an unexpected contract id {}, return OK so other side doesn't keep sending this", - event_receiver.local_addr, - event.contract_id - ); - request - .respond(HttpResponse::empty(200u16)) - .expect("response failed"); - return Err(EventError::UnrecognizedStackerDBContract(event.contract_id)); - } - - request - .respond(HttpResponse::empty(200u16)) - .expect("response failed"); - - Ok(SignerEvent::StackerDB(event)) + process_stackerdb_event(event_receiver.local_addr, request, is_mainnet) } else if request.url() == "/proposal_response" { - debug!("Got proposal_response event"); - let mut body = String::new(); - if let Err(e) = request - .as_reader() - .read_to_string(&mut body) { - error!("Failed to read body: {:?}", &e); - - request - .respond(HttpResponse::empty(200u16)) - .expect("response failed"); - return Err(EventError::MalformedRequest(format!( - "Failed to read body: {:?}", - &e - ))); - } - - let event: BlockValidateResponse = - serde_json::from_slice(body.as_bytes()).map_err(|e| { - EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)) - })?; - - request - .respond(HttpResponse::empty(200u16)) - .expect("response failed"); - - Ok(SignerEvent::BlockProposal(event)) + process_proposal_response(request) } else { let url = request.url().to_string(); @@ -349,3 +435,86 @@ impl EventReceiver for SignerEventReceiver { } } } + +/// Process a stackerdb event from the node +fn process_stackerdb_event( + local_addr: Option, + mut request: HttpRequest, + is_mainnet: bool, +) -> Result { + debug!("Got stackerdb_chunks event"); + let mut body = String::new(); + if let Err(e) = request.as_reader().read_to_string(&mut body) { + error!("Failed to read body: {:?}", &e); + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::MalformedRequest(format!( + "Failed to read body: {:?}", + &e + ))); + } + + let event: StackerDBChunksEvent = serde_json::from_slice(body.as_bytes()) + .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 = event + .modified_slots + .iter() + .filter_map(|chunk| read_next::(&mut &chunk.data[..]).ok()) + .collect(); + SignerEvent::ProposedBlocks(blocks) + } else if event.contract_id.name.to_string() == SIGNERS_NAME { + // TODO: fix this to be against boot_code_id(SIGNERS_NAME, is_mainnet) when .signers is deployed + let signer_messages: Vec = event + .modified_slots + .iter() + .filter_map(|chunk| bincode::deserialize::(&chunk.data).ok()) + .collect(); + SignerEvent::SignerMessages(signer_messages) + } else { + info!( + "[{:?}] next_event got event from an unexpected contract id {}, return OK so other side doesn't keep sending this", + local_addr, + event.contract_id + ); + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::UnrecognizedStackerDBContract(event.contract_id)); + }; + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + + Ok(signer_event) +} + +/// Process a proposal response from the node +fn process_proposal_response(mut request: HttpRequest) -> Result { + debug!("Got proposal_response event"); + let mut body = String::new(); + if let Err(e) = request.as_reader().read_to_string(&mut body) { + error!("Failed to read body: {:?}", &e); + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + return Err(EventError::MalformedRequest(format!( + "Failed to read body: {:?}", + &e + ))); + } + + let event: BlockValidateResponse = serde_json::from_slice(body.as_bytes()) + .map_err(|e| EventError::Deserialize(format!("Could not decode body to JSON: {:?}", &e)))?; + + request + .respond(HttpResponse::empty(200u16)) + .expect("response failed"); + + Ok(SignerEvent::BlockValidationResponse(event)) +} diff --git a/libsigner/src/libsigner.rs b/libsigner/src/libsigner.rs index b7f983f8c..8b10b3faf 100644 --- a/libsigner/src/libsigner.rs +++ b/libsigner/src/libsigner.rs @@ -44,7 +44,8 @@ mod session; pub use crate::error::{EventError, RPCError}; pub use crate::events::{ - EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, SignerStopSignaler, + BlockRejection, BlockResponse, EventReceiver, EventStopSignaler, RejectCode, SignerEvent, + SignerEventReceiver, SignerMessage, SignerStopSignaler, BLOCK_SLOT_ID, SIGNER_SLOTS_PER_USER, }; pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop}; pub use crate::session::{SignerSession, StackerDBSession}; diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 0048b7435..deefc1018 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -22,13 +22,16 @@ use std::sync::mpsc::{channel, Receiver, Sender}; use std::time::Duration; use std::{mem, thread}; +use blockstack_lib::chainstate::stacks::boot::SIGNERS_NAME; use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; +use blockstack_lib::util_lib::boot::boot_code_id; use clarity::vm::types::QualifiedContractIdentifier; use libstackerdb::StackerDBChunkData; use stacks_common::util::secp256k1::Secp256k1PrivateKey; use stacks_common::util::sleep_ms; +use wsts::net::{DkgBegin, Packet}; -use crate::events::SignerEvent; +use crate::events::{SignerEvent, SignerMessage}; use crate::{Signer, SignerEventReceiver, SignerRunLoop}; /// Simple runloop implementation. It receives `max_events` events and returns `events` from the @@ -87,28 +90,27 @@ impl SignerRunLoop, Command> for SimpleRunLoop { /// and the signer runloop. #[test] fn test_simple_signer() { - let ev = SignerEventReceiver::new(vec![QualifiedContractIdentifier::parse( - "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world", - ) - .unwrap()]); + let contract_id = + QualifiedContractIdentifier::parse("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.signers") + .unwrap(); // TODO: change to boot_code_id(SIGNERS_NAME, false) when .signers is deployed + let ev = SignerEventReceiver::new(vec![contract_id.clone()], false); let (_cmd_send, cmd_recv) = channel(); let (res_send, _res_recv) = channel(); let mut signer = Signer::new(SimpleRunLoop::new(5), ev, cmd_recv, res_send); let endpoint: SocketAddr = "127.0.0.1:30000".parse().unwrap(); - let mut chunks = vec![]; for i in 0..5 { let privk = Secp256k1PrivateKey::new(); - let mut chunk = StackerDBChunkData::new(i as u32, 1, "hello world".as_bytes().to_vec()); + let msg = wsts::net::Message::DkgBegin(DkgBegin { dkg_id: 0 }); + let message = SignerMessage::Packet(Packet { msg, sig: vec![] }); + let message_bytes = bincode::serialize(&message).unwrap(); + let mut chunk = StackerDBChunkData::new(i as u32, 1, message_bytes); chunk.sign(&privk).unwrap(); - let chunk_event = SignerEvent::StackerDB(StackerDBChunksEvent { - contract_id: QualifiedContractIdentifier::parse( - "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world", - ) - .unwrap(), + let chunk_event = StackerDBChunksEvent { + contract_id: contract_id.clone(), modified_slots: vec![chunk], - }); + }; chunks.push(chunk_event); } @@ -126,42 +128,38 @@ fn test_simple_signer() { } }; - match &thread_chunks[num_sent] { - SignerEvent::StackerDB(ev) => { - 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); - debug!("Send:\n{}", &req); + 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); + debug!("Send:\n{}", &req); - sock.write_all(req.as_bytes()).unwrap(); - sock.flush().unwrap(); + sock.write_all(req.as_bytes()).unwrap(); + sock.flush().unwrap(); - num_sent += 1; - } - _ => panic!("Unexpected event type"), - } + num_sent += 1; } }); let running_signer = signer.spawn(endpoint).unwrap(); sleep_ms(5000); - let mut accepted_events = running_signer.stop().unwrap(); + let accepted_events = running_signer.stop().unwrap(); - chunks.sort_by(|ev1, ev2| match (ev1, ev2) { - (SignerEvent::StackerDB(ev1), SignerEvent::StackerDB(ev2)) => ev1.modified_slots[0] + chunks.sort_by(|ev1, ev2| { + ev1.modified_slots[0] .slot_id .partial_cmp(&ev2.modified_slots[0].slot_id) - .unwrap(), - _ => panic!("Unexpected event type"), - }); - accepted_events.sort_by(|ev1, ev2| match (ev1, ev2) { - (SignerEvent::StackerDB(ev1), SignerEvent::StackerDB(ev2)) => ev1.modified_slots[0] - .slot_id - .partial_cmp(&ev2.modified_slots[0].slot_id) - .unwrap(), - _ => panic!("Unexpected event type"), + .unwrap() }); - // runloop got the event that the mocked stacks node sent - assert_eq!(accepted_events, chunks); + let sent_events: Vec = chunks + .iter() + .map(|chunk| { + let msg = chunk.modified_slots[0].data.clone(); + let signer_message: SignerMessage = bincode::deserialize(&msg).unwrap(); + SignerEvent::SignerMessages(vec![signer_message]) + }) + .collect(); + + assert_eq!(sent_events, accepted_events); mock_stacks_node.join().unwrap(); } diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 431c772e5..e5bdfd09f 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -13,161 +13,18 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::net::api::postblock_proposal::{BlockValidateReject, ValidateRejectCode}; use clarity::vm::types::QualifiedContractIdentifier; use hashbrown::HashMap; -use libsigner::{SignerSession, StackerDBSession}; +use libsigner::{SignerMessage, SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; -use serde_derive::{Deserialize, Serialize}; use slog::{slog_debug, slog_warn}; use stacks_common::types::chainstate::StacksPrivateKey; use stacks_common::{debug, warn}; -use wsts::net::{Message, Packet}; use super::ClientError; use crate::client::retry_with_exponential_backoff; use crate::config::Config; -/// Temporary placeholder for the number of slots allocated to a stacker-db writer. This will be retrieved from the stacker-db instance in the future -/// See: https://github.com/stacks-network/stacks-blockchain/issues/3921 -/// Is equal to the number of message types -pub const SIGNER_SLOTS_PER_USER: u32 = 11; - -// The slot IDS for each message type -const DKG_BEGIN_SLOT_ID: u32 = 0; -const DKG_PRIVATE_BEGIN_SLOT_ID: u32 = 1; -const DKG_END_BEGIN_SLOT_ID: u32 = 2; -const DKG_END_SLOT_ID: u32 = 3; -const DKG_PUBLIC_SHARES_SLOT_ID: u32 = 4; -const DKG_PRIVATE_SHARES_SLOT_ID: u32 = 5; -const NONCE_REQUEST_SLOT_ID: u32 = 6; -const NONCE_RESPONSE_SLOT_ID: u32 = 7; -const SIGNATURE_SHARE_REQUEST_SLOT_ID: u32 = 8; -const SIGNATURE_SHARE_RESPONSE_SLOT_ID: u32 = 9; -const BLOCK_SLOT_ID: u32 = 10; - -/// The messages being sent through the stacker db contracts -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum SignerMessage { - /// The signed/validated Nakamoto block for miners to observe - BlockResponse(BlockResponse), - /// DKG and Signing round data for other signers to observe - Packet(Packet), -} - -/// The response that a signer sends back to observing miners -/// either accepting or rejecting a Nakamoto block with the corresponding reason -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum BlockResponse { - /// The Nakamoto block was accepted and therefore signed - Accepted(NakamotoBlock), - /// The Nakamoto block was rejected and therefore not signed - Rejected(BlockRejection), -} - -/// A rejection response from a signer for a proposed block -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct BlockRejection { - /// The reason for the rejection - pub reason: String, - /// The reason code for the rejection - pub reason_code: RejectCode, - /// The block that was rejected - pub block: NakamotoBlock, -} - -impl BlockRejection { - /// Create a new BlockRejection for the provided block and reason code - pub fn new(block: NakamotoBlock, reason_code: RejectCode) -> Self { - Self { - reason: reason_code.to_string(), - reason_code, - block, - } - } -} - -impl From for BlockRejection { - fn from(reject: BlockValidateReject) -> Self { - Self { - reason: reject.reason, - reason_code: RejectCode::ValidationFailed(reject.reason_code), - block: reject.block, - } - } -} - -/// This enum is used to supply a `reason_code` for block rejections -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[repr(u8)] -pub enum RejectCode { - /// RPC endpoint Validation failed - ValidationFailed(ValidateRejectCode), - /// Signers signed a block rejection - SignedRejection, - /// Invalid signature hash - InvalidSignatureHash, -} - -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::InvalidSignatureHash => write!(f, "The signature hash was invalid."), - } - } -} - -impl From for SignerMessage { - fn from(packet: Packet) -> Self { - Self::Packet(packet) - } -} - -impl From for SignerMessage { - fn from(block_response: BlockResponse) -> Self { - Self::BlockResponse(block_response) - } -} - -impl From for SignerMessage { - fn from(block_rejection: BlockRejection) -> Self { - Self::BlockResponse(BlockResponse::Rejected(block_rejection)) - } -} - -impl From for SignerMessage { - fn from(rejection: BlockValidateReject) -> Self { - Self::BlockResponse(BlockResponse::Rejected(rejection.into())) - } -} - -impl SignerMessage { - /// Helper function to determine the slot ID for the provided stacker-db writer id - pub fn slot_id(&self, id: u32) -> u32 { - let slot_id = match self { - Self::Packet(packet) => match packet.msg { - Message::DkgBegin(_) => DKG_BEGIN_SLOT_ID, - Message::DkgPrivateBegin(_) => DKG_PRIVATE_BEGIN_SLOT_ID, - Message::DkgEndBegin(_) => DKG_END_BEGIN_SLOT_ID, - Message::DkgEnd(_) => DKG_END_SLOT_ID, - Message::DkgPublicShares(_) => DKG_PUBLIC_SHARES_SLOT_ID, - Message::DkgPrivateShares(_) => DKG_PRIVATE_SHARES_SLOT_ID, - Message::NonceRequest(_) => NONCE_REQUEST_SLOT_ID, - Message::NonceResponse(_) => NONCE_RESPONSE_SLOT_ID, - Message::SignatureShareRequest(_) => SIGNATURE_SHARE_REQUEST_SLOT_ID, - Message::SignatureShareResponse(_) => SIGNATURE_SHARE_RESPONSE_SLOT_ID, - }, - Self::BlockResponse(_) => BLOCK_SLOT_ID, - }; - SIGNER_SLOTS_PER_USER * id + slot_id - } -} - /// The StackerDB client for communicating with the .signers contract pub struct StackerDB { /// The stacker-db session for the signer StackerDB diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 58774b770..c9d086df3 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -91,6 +91,14 @@ impl Network { Self::Testnet | Self::Mocknet => TransactionVersion::Testnet, } } + + /// Check if the network is Mainnet or not + pub fn is_mainnet(&self) -> bool { + match self { + Self::Mainnet => true, + Self::Testnet | Self::Mocknet => false, + } + } } /// The parsed configuration for the signer diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index 4fadef279..99bfb31d0 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -36,7 +36,10 @@ use std::time::Duration; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use clap::Parser; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{RunningSigner, Signer, SignerEventReceiver, SignerSession, StackerDBSession}; +use libsigner::{ + RunningSigner, Signer, SignerEventReceiver, SignerSession, StackerDBSession, + SIGNER_SLOTS_PER_USER, +}; use libstackerdb::StackerDBChunkData; use slog::{slog_debug, slog_error}; use stacks_common::address::{ @@ -49,7 +52,6 @@ use stacks_signer::cli::{ Cli, Command, GenerateFilesArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, SignArgs, StackerDBArgs, }; -use stacks_signer::client::SIGNER_SLOTS_PER_USER; use stacks_signer::config::{Config, Network}; use stacks_signer::runloop::{RunLoop, RunLoopCommand}; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; @@ -90,7 +92,10 @@ fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { let config = Config::try_from(path).unwrap(); let (cmd_send, cmd_recv) = channel(); let (res_send, res_recv) = channel(); - let ev = SignerEventReceiver::new(vec![config.stackerdb_contract_id.clone()]); + let ev = SignerEventReceiver::new( + vec![config.stackerdb_contract_id.clone()], + config.network.is_mainnet(), + ); let runloop: RunLoop> = RunLoop::from(&config); let mut signer: Signer< RunLoopCommand, diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 2465b5753..e7b7ae008 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -19,14 +19,12 @@ use std::time::Duration; use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::nakamoto::NakamotoBlock; -use blockstack_lib::chainstate::stacks::boot::MINERS_NAME; -use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; use blockstack_lib::chainstate::stacks::ThresholdSignature; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; -use blockstack_lib::util_lib::boot::boot_code_id; use hashbrown::{HashMap, HashSet}; -use libsigner::{SignerEvent, SignerRunLoop}; -use libstackerdb::StackerDBChunkData; +use libsigner::{ + BlockRejection, BlockResponse, RejectCode, SignerEvent, SignerMessage, SignerRunLoop, +}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::util::hash::{Sha256Sum, Sha512Trunc256Sum}; @@ -41,10 +39,7 @@ use wsts::state_machine::signer::Signer; use wsts::state_machine::{OperationResult, PublicKeys}; use wsts::v2; -use crate::client::{ - retry_with_exponential_backoff, BlockRejection, BlockResponse, ClientError, RejectCode, - SignerMessage, StackerDB, StacksClient, -}; +use crate::client::{retry_with_exponential_backoff, ClientError, StackerDB, StacksClient}; use crate::config::{Config, Network}; /// Which operation to perform @@ -328,40 +323,31 @@ impl RunLoop { } } - /// Handle the stackerdb chunk event as a signer message - fn handle_stackerdb_chunk_event_signers( + /// Handle signer messages submitted to signers stackerdb + fn handle_signer_messages( &mut self, - stackerdb_chunk_event: StackerDBChunksEvent, res: Sender>, + messages: Vec, ) { let (_coordinator_id, coordinator_public_key) = calculate_coordinator(&self.signing_round.public_keys, &self.stacks_client); - - let inbound_packets: Vec = stackerdb_chunk_event - .modified_slots - .iter() - .filter_map(|chunk| self.verify_chunk(chunk, &coordinator_public_key)) + let packets: Vec = messages + .into_iter() + .filter_map(|msg| match msg { + SignerMessage::BlockResponse(_) => None, + SignerMessage::Packet(packet) => { + self.verify_packet(packet, &coordinator_public_key) + } + }) .collect(); - self.handle_packets(res, &inbound_packets); + self.handle_packets(res, &packets); } - /// Handle the stackerdb chunk event as a miner message - fn handle_stackerdb_chunk_event_miners(&mut self, stackerdb_chunk_event: StackerDBChunksEvent) { - for chunk in &stackerdb_chunk_event.modified_slots { - let Some(block) = read_next::(&mut &chunk.data[..]).ok() else { - warn!("Received an unrecognized message type from .miners stacker-db slot id {}: {:?}", chunk.slot_id, chunk.data); - continue; - }; + /// Handle proposed blocks submitted by the miners to stackerdb + fn handle_proposed_blocks(&mut self, blocks: Vec) { + for block in blocks { let Ok(hash) = block.header.signature_hash() else { - warn!("Received a block proposal with an invalid signature hash. Broadcasting a block rejection..."); - 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); - } + self.broadcast_signature_hash_rejection(block); continue; }; // Store the block in our cache @@ -517,17 +503,12 @@ impl RunLoop { /// and SignatureShareRequests with a different message than what the coordinator originally sent. /// This is done to prevent a malicious coordinator from sending a different message than what was /// agreed upon and to support the case where the signer wishes to reject a block by voting no - fn verify_chunk( + fn verify_packet( &mut self, - chunk: &StackerDBChunkData, + mut packet: Packet, coordinator_public_key: &PublicKey, ) -> Option { - // We only care about verified wsts packets. Ignore anything else - let signer_message = bincode::deserialize::(&chunk.data).ok()?; - let mut packet = match signer_message { - SignerMessage::Packet(packet) => packet, - _ => return None, // This is a message for miners to observe. Ignore it. - }; + // We only care about verified wsts packets. Ignore anything else. if packet.verify(&self.signing_round.public_keys, coordinator_public_key) { match &mut packet.msg { Message::SignatureShareRequest(request) => { @@ -764,26 +745,17 @@ impl SignerRunLoop, RunLoopCommand> for Run // Process any arrived events debug!("Processing event: {:?}", event); match event { - Some(SignerEvent::BlockProposal(block_validate_response)) => { + Some(SignerEvent::BlockValidationResponse(block_validate_response)) => { debug!("Received a block proposal result from the stacks node..."); self.handle_block_validate_response(block_validate_response, res) } - Some(SignerEvent::StackerDB(stackerdb_chunk_event)) => { - if stackerdb_chunk_event.contract_id == *self.stackerdb.signers_contract_id() { - debug!("Received a StackerDB event for the .signers contract..."); - self.handle_stackerdb_chunk_event_signers(stackerdb_chunk_event, res); - } else if stackerdb_chunk_event.contract_id - == boot_code_id(MINERS_NAME, self.mainnet) - { - debug!("Received a StackerDB event for the .miners contract..."); - self.handle_stackerdb_chunk_event_miners(stackerdb_chunk_event); - } else { - // Ignore non miner or signer messages - debug!( - "Received a StackerDB event for an unrecognized contract id: {:?}. Ignoring...", - stackerdb_chunk_event.contract_id - ); - } + Some(SignerEvent::SignerMessages(messages)) => { + debug!("Received messages from the other signers..."); + self.handle_signer_messages(res, messages); + } + Some(SignerEvent::ProposedBlocks(blocks)) => { + debug!("Received block proposals from the miners..."); + self.handle_proposed_blocks(blocks); } None => { // No event. Do nothing. diff --git a/stackslib/src/chainstate/stacks/boot/mod.rs b/stackslib/src/chainstate/stacks/boot/mod.rs index da7c97634..47042478a 100644 --- a/stackslib/src/chainstate/stacks/boot/mod.rs +++ b/stackslib/src/chainstate/stacks/boot/mod.rs @@ -88,6 +88,7 @@ pub const BOOT_TEST_POX_4_AGG_KEY_CONTRACT: &'static str = "pox-4-agg-test-boote pub const BOOT_TEST_POX_4_AGG_KEY_FNAME: &'static str = "aggregate-key"; pub const MINERS_NAME: &'static str = "miners"; +pub const SIGNERS_NAME: &'static str = "signers"; pub mod docs; diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 0a2c78af7..a62b53985 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -6,20 +6,21 @@ use std::time::{Duration, Instant}; use std::{env, thread}; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{RunningSigner, Signer, SignerEventReceiver}; +use libsigner::{ + BlockResponse, RunningSigner, Signer, SignerEventReceiver, SignerMessage, BLOCK_SLOT_ID, + SIGNER_SLOTS_PER_USER, +}; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; -use stacks::chainstate::stacks::boot::MINERS_NAME; use stacks::chainstate::stacks::{StacksPrivateKey, ThresholdSignature}; use stacks::net::api::postblock_proposal::BlockValidateResponse; -use stacks::util_lib::boot::boot_code_id; use stacks_common::types::chainstate::{ ConsensusHash, StacksAddress, StacksBlockId, StacksPublicKey, TrieHash, }; use stacks_common::util::hash::{MerkleTree, Sha512Trunc256Sum}; use stacks_common::util::secp256k1::MessageSignature; -use stacks_signer::client::{BlockResponse, SignerMessage, StacksClient, SIGNER_SLOTS_PER_USER}; -use stacks_signer::config::{Config as SignerConfig, Network}; +use stacks_signer::client::StacksClient; +use stacks_signer::config::Config as SignerConfig; use stacks_signer::runloop::{calculate_coordinator, RunLoopCommand}; use stacks_signer::utils::{build_signer_config_tomls, build_stackerdb_contract}; use tracing_subscriber::prelude::*; @@ -63,7 +64,7 @@ struct SignerTest { // The channel for sending commands to the coordinator pub coordinator_cmd_sender: Sender, // The channels for sending commands to the signers - pub signer_cmd_senders: HashMap>, + pub _signer_cmd_senders: HashMap>, // The channels for receiving results from both the coordinator and the signers pub result_receivers: Vec>>, // The running coordinator and its threads @@ -152,7 +153,7 @@ impl SignerTest { Self { running_nodes: node, result_receivers, - signer_cmd_senders, + _signer_cmd_senders: signer_cmd_senders, coordinator_cmd_sender, running_coordinator, running_signers, @@ -186,10 +187,8 @@ fn spawn_signer( sender: Sender>, ) -> RunningSigner> { let config = stacks_signer::config::Config::load_from_str(data).unwrap(); - let ev = SignerEventReceiver::new(vec![ - boot_code_id(MINERS_NAME, config.network == Network::Mainnet), - config.stackerdb_contract_id.clone(), - ]); + let is_mainnet = config.network.is_mainnet(); + let ev = SignerEventReceiver::new(vec![config.stackerdb_contract_id.clone()], is_mainnet); let runloop: stacks_signer::runloop::RunLoop> = stacks_signer::runloop::RunLoop::from(&config); let mut signer: Signer< @@ -666,7 +665,7 @@ fn stackerdb_block_proposal() { for event in nakamoto_blocks { // The tenth slot is the miners block slot for slot in event.modified_slots { - if slot.slot_id == 10 { + if slot.slot_id == BLOCK_SLOT_ID { chunk = Some(slot.data); break; }