diff --git a/libsigner/src/error.rs b/libsigner/src/error.rs index 101a1b35e..7c4deadf1 100644 --- a/libsigner/src/error.rs +++ b/libsigner/src/error.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -71,4 +71,7 @@ pub enum EventError { /// Unrecognized stacker DB contract error #[error("Unrecognized StackerDB contract: {0}")] UnrecognizedStackerDBContract(QualifiedContractIdentifier), + /// Empty chunks event + #[error("Empty chunks event")] + EmptyChunksEvent, } diff --git a/libsigner/src/events.rs b/libsigner/src/events.rs index 67eb97057..c603db7f0 100644 --- a/libsigner/src/events.rs +++ b/libsigner/src/events.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::fmt::Debug; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -51,11 +52,19 @@ use wsts::net::{ use wsts::state_machine::signer; use crate::http::{decode_http_body, decode_http_request}; -use crate::{EventError, SignerMessage}; +use crate::EventError; + +/// Define the trait for the event processor +pub trait SignerEventTrait: + StacksMessageCodec + Clone + Debug + Send +{ +} + +impl SignerEventTrait for T {} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] /// BlockProposal sent to signers -pub struct BlockProposalSigners { +pub struct BlockProposal { /// The block itself pub block: NakamotoBlock, /// The burn height the block is mined during @@ -64,25 +73,7 @@ pub struct BlockProposalSigners { pub reward_cycle: u64, } -/// Event enum for newly-arrived signer subscribed events -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum SignerEvent { - /// A miner sent a message over .miners - /// The `Vec` will contain any signer WSTS messages made by the miner while acting as a coordinator. - /// The `Option` will contain the message sender's public key if the vec is non-empty. - MinerMessages(Vec, Option), - /// 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), - /// A new block proposal validation response from the node - BlockValidationResponse(BlockValidateResponse), - /// Status endpoint request - StatusCheck, - /// A new burn block event was received with the given burnchain block height - NewBurnBlock(u64), -} - -impl StacksMessageCodec for BlockProposalSigners { +impl StacksMessageCodec for BlockProposal { fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { self.block.consensus_serialize(fd)?; self.burn_height.consensus_serialize(fd)?; @@ -94,7 +85,7 @@ impl StacksMessageCodec for BlockProposalSigners { let block = NakamotoBlock::consensus_deserialize(fd)?; let burn_height = u64::consensus_deserialize(fd)?; let reward_cycle = u64::consensus_deserialize(fd)?; - Ok(BlockProposalSigners { + Ok(BlockProposal { block, burn_height, reward_cycle, @@ -102,6 +93,24 @@ impl StacksMessageCodec for BlockProposalSigners { } } +/// Event enum for newly-arrived signer subscribed events +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum SignerEvent { + /// A miner sent a message over .miners + /// The `Vec` will contain any signer messages made by the miner. + /// The `StacksPublicKey` is the message sender's public key. + MinerMessages(Vec, StacksPublicKey), + /// 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), + /// A new block proposal validation response from the node + BlockValidationResponse(BlockValidateResponse), + /// Status endpoint request + StatusCheck, + /// A new burn block event was received with the given burnchain block height + NewBurnBlock(u64), +} + /// 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. @@ -111,7 +120,7 @@ pub trait EventStopSignaler { } /// Trait to implement to handle signer specific events sent by the Stacks node -pub trait EventReceiver { +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. type ST: EventStopSignaler + Send + Sync; @@ -119,11 +128,11 @@ pub trait EventReceiver { /// Open a server socket to the given socket address. fn bind(&mut self, listener: SocketAddr) -> Result; /// Return the next event - fn next_event(&mut self) -> Result; + fn next_event(&mut self) -> Result, EventError>; /// Add a downstream event consumer - fn add_consumer(&mut self, event_out: Sender); + fn add_consumer(&mut self, event_out: Sender>); /// Forward the event to downstream consumers - fn forward_event(&mut self, ev: SignerEvent) -> bool; + fn forward_event(&mut self, ev: SignerEvent) -> bool; /// Determine if the receiver should hang up fn is_stopped(&self) -> bool; /// Get a stop signal instance that, when sent, will cause this receiver to stop accepting new @@ -164,23 +173,23 @@ pub trait EventReceiver { } /// Event receiver for Signer events -pub struct SignerEventReceiver { +pub struct SignerEventReceiver { /// Address we bind to local_addr: Option, /// server socket that listens for HTTP POSTs from the node http_server: Option, /// channel into which to write newly-discovered data - out_channels: Vec>, + 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 { +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(is_mainnet: bool) -> SignerEventReceiver { + pub fn new(is_mainnet: bool) -> SignerEventReceiver { SignerEventReceiver { http_server: None, local_addr: None, @@ -193,7 +202,7 @@ impl SignerEventReceiver { /// Do something with the socket pub fn with_server(&mut self, todo: F) -> Result where - F: FnOnce(&SignerEventReceiver, &mut HttpServer, bool) -> R, + F: FnOnce(&SignerEventReceiver, &mut HttpServer, bool) -> R, { let mut server = if let Some(s) = self.http_server.take() { s @@ -225,6 +234,7 @@ impl SignerStopSignaler { } impl EventStopSignaler for SignerStopSignaler { + #[cfg_attr(test, mutants::skip)] fn send(&mut self) { self.stop_signal.store(true, Ordering::SeqCst); // wake up the thread so the atomicbool can be checked @@ -238,15 +248,14 @@ impl EventStopSignaler for SignerStopSignaler { body.len(), body ); - match stream.write_all(req.as_bytes()) { - Err(e) => error!("Failed to send shutdown request: {}", e), - _ => (), - }; + if let Err(e) = stream.write_all(req.as_bytes()) { + error!("Failed to send shutdown request: {}", e); + } } } } -impl EventReceiver for SignerEventReceiver { +impl EventReceiver for SignerEventReceiver { type ST = SignerStopSignaler; /// Start listening on the given socket address. @@ -261,7 +270,7 @@ impl EventReceiver for SignerEventReceiver { /// Wait for the node to post something, and then return it. /// Errors are recoverable -- the caller should call this method again even if it returns an /// error. - fn next_event(&mut self) -> Result { + fn next_event(&mut self) -> Result, EventError> { self.with_server(|event_receiver, http_server, _is_mainnet| { // were we asked to terminate? if event_receiver.is_stopped() { @@ -318,7 +327,7 @@ impl EventReceiver for SignerEventReceiver { /// Forward an event /// Return true on success; false on error. /// Returning false terminates the event receiver. - fn forward_event(&mut self, ev: SignerEvent) -> bool { + fn forward_event(&mut self, ev: SignerEvent) -> bool { if self.out_channels.is_empty() { // nothing to do error!("No channels connected to event receiver"); @@ -342,7 +351,7 @@ impl EventReceiver for SignerEventReceiver { } /// Add an event consumer. A received event will be forwarded to this Sender. - fn add_consumer(&mut self, out_channel: Sender) { + fn add_consumer(&mut self, out_channel: Sender>) { self.out_channels.push(out_channel); } @@ -367,10 +376,10 @@ fn ack_dispatcher(request: HttpRequest) { } /// Process a stackerdb event from the node -fn process_stackerdb_event( +fn process_stackerdb_event( local_addr: Option, mut request: HttpRequest, -) -> Result { +) -> Result, EventError> { debug!("Got stackerdb_chunks event"); let mut body = String::new(); if let Err(e) = request.as_reader().read_to_string(&mut body) { @@ -395,7 +404,7 @@ fn process_stackerdb_event( event_contract_id ); ack_dispatcher(request); - return Err(e.into()); + return Err(e); } Ok(x) => x, }; @@ -405,7 +414,7 @@ fn process_stackerdb_event( Ok(signer_event) } -impl TryFrom for SignerEvent { +impl TryFrom for SignerEvent { type Error = EventError; fn try_from(event: StackerDBChunksEvent) -> Result { @@ -415,18 +424,18 @@ impl TryFrom for SignerEvent { let mut messages = vec![]; let mut miner_pk = None; for chunk in event.modified_slots { + let Ok(msg) = T::consensus_deserialize(&mut chunk.data.as_slice()) else { + continue; + }; + miner_pk = Some(chunk.recover_pk().map_err(|e| { EventError::MalformedRequest(format!( "Failed to recover PK from StackerDB chunk: {e}" )) })?); - let Ok(msg) = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - else { - continue; - }; messages.push(msg); } - SignerEvent::MinerMessages(messages, miner_pk) + SignerEvent::MinerMessages(messages, miner_pk.ok_or(EventError::EmptyChunksEvent)?) } else if event.contract_id.name.starts_with(SIGNERS_NAME) && event.contract_id.is_boot() { let Some((signer_set, _)) = get_signers_db_signer_set_message_id(event.contract_id.name.as_str()) @@ -434,10 +443,10 @@ impl TryFrom for SignerEvent { return Err(EventError::UnrecognizedStackerDBContract(event.contract_id)); }; // signer-XXX-YYY boot contract - let signer_messages: Vec = event + let signer_messages: Vec = event .modified_slots .iter() - .filter_map(|chunk| read_next::(&mut &chunk.data[..]).ok()) + .filter_map(|chunk| read_next::(&mut &chunk.data[..]).ok()) .collect(); SignerEvent::SignerMessages(signer_set, signer_messages) } else { @@ -448,7 +457,9 @@ impl TryFrom for SignerEvent { } /// Process a proposal response from the node -fn process_proposal_response(mut request: HttpRequest) -> Result { +fn process_proposal_response( + mut request: HttpRequest, +) -> Result, EventError> { debug!("Got proposal_response event"); let mut body = String::new(); if let Err(e) = request.as_reader().read_to_string(&mut body) { @@ -474,7 +485,9 @@ fn process_proposal_response(mut request: HttpRequest) -> Result Result { +fn process_new_burn_block_event( + mut request: HttpRequest, +) -> Result, EventError> { debug!("Got burn_block event"); let mut body = String::new(); if let Err(e) = request.as_reader().read_to_string(&mut body) { diff --git a/libsigner/src/http.rs b/libsigner/src/http.rs index fe841415a..adb1f509f 100644 --- a/libsigner/src/http.rs +++ b/libsigner/src/http.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/libsigner/src/libsigner.rs b/libsigner/src/libsigner.rs index 0b16e97e1..874ebad1f 100644 --- a/libsigner/src/libsigner.rs +++ b/libsigner/src/libsigner.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -39,18 +39,18 @@ mod tests; mod error; mod events; mod http; -mod messages; mod runloop; mod session; mod signer_set; +/// v0 signer related code +pub mod v0; +/// v1 signer related code +pub mod v1; pub use crate::error::{EventError, RPCError}; pub use crate::events::{ - BlockProposalSigners, EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, - SignerStopSignaler, -}; -pub use crate::messages::{ - BlockRejection, BlockResponse, MessageSlotID, RejectCode, SignerMessage, + BlockProposal, EventReceiver, EventStopSignaler, SignerEvent, SignerEventReceiver, + SignerEventTrait, SignerStopSignaler, }; pub use crate::runloop::{RunningSigner, Signer, SignerRunLoop}; pub use crate::session::{SignerSession, StackerDBSession}; diff --git a/libsigner/src/runloop.rs b/libsigner/src/runloop.rs index 0b7eb2dbc..b0f026f35 100644 --- a/libsigner/src/runloop.rs +++ b/libsigner/src/runloop.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -24,11 +24,12 @@ use std::thread; use std::thread::JoinHandle; use std::time::Duration; +use clarity::codec::StacksMessageCodec; use stacks_common::deps_common::ctrlc as termination; use stacks_common::deps_common::ctrlc::SignalId; use crate::error::EventError; -use crate::events::{EventReceiver, EventStopSignaler, SignerEvent}; +use crate::events::{EventReceiver, EventStopSignaler, SignerEvent, SignerEventTrait}; /// Some libcs, like musl, have a very small stack size. /// Make sure it's big enough. @@ -40,7 +41,7 @@ const STDERR: i32 = 2; /// Trait describing the needful components of a top-level runloop. /// This is where the signer business logic would go. /// Implement this, and you get all the multithreaded setup for free. -pub trait SignerRunLoop { +pub trait SignerRunLoop { /// Hint to set how long to wait for new events fn set_event_timeout(&mut self, timeout: Duration); /// Getter for the event poll timeout @@ -50,7 +51,7 @@ pub trait SignerRunLoop { /// Returns None to keep running. fn run_one_pass( &mut self, - event: Option, + event: Option>, cmd: Option, res: Sender, ) -> Option; @@ -64,7 +65,7 @@ pub trait SignerRunLoop { /// This would run in a separate thread from the event receiver. fn main_loop( &mut self, - event_recv: Receiver, + event_recv: Receiver>, command_recv: Receiver, result_send: Sender, mut event_stop_signaler: EVST, @@ -93,7 +94,7 @@ pub trait SignerRunLoop { } /// The top-level signer implementation -pub struct Signer { +pub struct Signer { /// the runloop itself signer_loop: Option, /// the event receiver to use @@ -102,10 +103,12 @@ pub struct Signer { command_receiver: Option>, /// the result sender to use result_sender: Option>, + /// phantom data for the codec + phantom_data: PhantomData, } /// The running signer implementation -pub struct RunningSigner { +pub struct RunningSigner, R, T: SignerEventTrait> { /// join handle for signer runloop signer_join: JoinHandle>, /// join handle for event receiver @@ -114,7 +117,7 @@ pub struct RunningSigner { stop_signal: EV::ST, } -impl RunningSigner { +impl, R, T: SignerEventTrait> RunningSigner { /// Stop the signer, and get the final state pub fn stop(mut self) -> Option { // kill event receiver @@ -189,19 +192,20 @@ pub fn set_runloop_signal_handler(mut st }).expect("FATAL: failed to set signal handler"); } -impl Signer { +impl Signer { /// Create a new signer with the given runloop and event receiver. pub fn new( runloop: SL, event_receiver: EV, command_receiver: Receiver, result_sender: Sender, - ) -> Signer { + ) -> Signer { Signer { signer_loop: Some(runloop), event_receiver: Some(event_receiver), command_receiver: Some(command_receiver), result_sender: Some(result_sender), + phantom_data: PhantomData, } } } @@ -209,9 +213,10 @@ impl Signer { impl< CMD: Send + 'static, R: Send + 'static, - SL: SignerRunLoop + Send + 'static, - EV: EventReceiver + Send + 'static, - > Signer + T: SignerEventTrait + 'static, + SL: SignerRunLoop + Send + 'static, + EV: EventReceiver + Send + 'static, + > Signer { /// This is a helper function to spawn both the runloop and event receiver in their own /// threads. Advanced signers may not need this method, and instead opt to run the receiver @@ -223,7 +228,7 @@ impl< /// /// On success, this method consumes the Signer and returns a RunningSigner with the relevant /// inter-thread communication primitives for the caller to shut down the system. - pub fn spawn(&mut self, bind_addr: SocketAddr) -> Result, EventError> { + pub fn spawn(&mut self, bind_addr: SocketAddr) -> Result, EventError> { let mut event_receiver = self .event_receiver .take() diff --git a/libsigner/src/session.rs b/libsigner/src/session.rs index 307801695..c13621392 100644 --- a/libsigner/src/session.rs +++ b/libsigner/src/session.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by diff --git a/libsigner/src/signer_set.rs b/libsigner/src/signer_set.rs index 119873fd1..fdcb857fa 100644 --- a/libsigner/src/signer_set.rs +++ b/libsigner/src/signer_set.rs @@ -86,7 +86,7 @@ impl SignerEntries { weight_end = weight_start + entry.weight; let key_ids: HashSet = (weight_start..weight_end).collect(); for key_id in key_ids.iter() { - wsts_key_ids.insert(*key_id, ecdsa_pk.clone()); + wsts_key_ids.insert(*key_id, ecdsa_pk); } signer_key_ids.insert(signer_id, (weight_start..weight_end).collect()); coordinator_key_ids.insert(signer_id, key_ids); diff --git a/libsigner/src/tests/http.rs b/libsigner/src/tests/http.rs index d2b052fae..d0f3887b4 100644 --- a/libsigner/src/tests/http.rs +++ b/libsigner/src/tests/http.rs @@ -264,7 +264,7 @@ fn test_run_http_request_with_body() { let result_chunked = run_http_request( &mut msock_chunked, - &"127.0.0.1:20443", + "127.0.0.1:20443", verb, path, content_type, @@ -275,7 +275,7 @@ fn test_run_http_request_with_body() { let result_plain = run_http_request( &mut msock_plain, - &"127.0.0.1:20443", + "127.0.0.1:20443", verb, path, content_type, @@ -321,7 +321,7 @@ fn test_run_http_request_no_body() { let result_chunked = run_http_request( &mut msock_chunked, - &"127.0.0.1:20443", + "127.0.0.1:20443", verb, path, content_type, @@ -330,7 +330,7 @@ fn test_run_http_request_no_body() { .unwrap(); let result_plain = run_http_request( &mut msock_plain, - &"127.0.0.1:20443", + "127.0.0.1:20443", verb, path, content_type, diff --git a/libsigner/src/tests/mod.rs b/libsigner/src/tests/mod.rs index 1d3e1f3cc..c584572ba 100644 --- a/libsigner/src/tests/mod.rs +++ b/libsigner/src/tests/mod.rs @@ -16,6 +16,7 @@ mod http; +use std::fmt::Debug; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -36,20 +37,20 @@ use stacks_common::util::secp256k1::Secp256k1PrivateKey; use stacks_common::util::sleep_ms; use wsts::net::{DkgBegin, Packet}; -use crate::events::SignerEvent; -use crate::messages::SignerMessage; +use crate::events::{SignerEvent, SignerEventTrait}; +use crate::v1::messages::SignerMessage; use crate::{Signer, SignerEventReceiver, SignerRunLoop}; /// Simple runloop implementation. It receives `max_events` events and returns `events` from the /// last call to `run_one_pass` as its final state. -struct SimpleRunLoop { +struct SimpleRunLoop { poll_timeout: Duration, - events: Vec, + events: Vec>, max_events: usize, } -impl SimpleRunLoop { - pub fn new(max_events: usize) -> SimpleRunLoop { +impl SimpleRunLoop { + pub fn new(max_events: usize) -> SimpleRunLoop { SimpleRunLoop { poll_timeout: Duration::from_millis(100), events: vec![], @@ -62,7 +63,7 @@ enum Command { Empty, } -impl SignerRunLoop, Command> for SimpleRunLoop { +impl SignerRunLoop>, Command, T> for SimpleRunLoop { fn set_event_timeout(&mut self, timeout: Duration) { self.poll_timeout = timeout; } @@ -73,10 +74,10 @@ impl SignerRunLoop, Command> for SimpleRunLoop { fn run_one_pass( &mut self, - event: Option, + event: Option>, _cmd: Option, - _res: Sender>, - ) -> Option> { + _res: Sender>>, + ) -> Option>> { debug!("Got event: {:?}", &event); if let Some(event) = event { self.events.push(event); @@ -161,7 +162,7 @@ fn test_simple_signer() { .unwrap() }); - let sent_events: Vec = chunks + let sent_events: Vec> = chunks .iter() .map(|chunk| { let msg = chunk.modified_slots[0].data.clone(); @@ -211,7 +212,7 @@ fn test_status_endpoint() { sleep_ms(3000); let accepted_events = running_signer.stop().unwrap(); - let sent_events: Vec = vec![SignerEvent::StatusCheck]; + let sent_events: Vec> = vec![SignerEvent::StatusCheck]; assert_eq!(sent_events, accepted_events); mock_stacks_node.join().unwrap(); diff --git a/libsigner/src/v0/messages.rs b/libsigner/src/v0/messages.rs new file mode 100644 index 000000000..4b9cd74dc --- /dev/null +++ b/libsigner/src/v0/messages.rs @@ -0,0 +1,572 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Messages in the signer-miner interaction have a multi-level hierarchy. +//! Signers send messages to each other through Packet messages. These messages, +//! as well as `BlockResponse`, `Transactions`, and `DkgResults` messages are stored +//! StackerDBs based on the `MessageSlotID` for the particular message type. This is a +//! shared identifier space between the four message kinds and their subtypes. +//! +//! These four message kinds are differentiated with a `SignerMessageTypePrefix` +//! and the `SignerMessage` enum. + +use std::fmt::{Debug, Display}; +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Sender; +use std::sync::Arc; + +use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners; +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; +use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent; +use blockstack_lib::chainstate::stacks::StacksTransaction; +use blockstack_lib::net::api::postblock_proposal::{ + BlockValidateReject, BlockValidateResponse, ValidateRejectCode, +}; +use blockstack_lib::util_lib::boot::boot_code_id; +use clarity::util::retry::BoundReader; +use clarity::util::secp256k1::MessageSignature; +use clarity::vm::types::serialization::SerializationError; +use clarity::vm::types::QualifiedContractIdentifier; +use hashbrown::{HashMap, HashSet}; +use serde::{Deserialize, Serialize}; +use stacks_common::codec::{ + read_next, read_next_at_most, read_next_exact, write_next, Error as CodecError, + StacksMessageCodec, +}; +use stacks_common::consts::SIGNER_SLOTS_PER_USER; +use stacks_common::util::hash::Sha512Trunc256Sum; +use tiny_http::{ + Method as HttpMethod, Request as HttpRequest, Response as HttpResponse, Server as HttpServer, +}; + +use crate::http::{decode_http_body, decode_http_request}; +use crate::{BlockProposal, EventError}; + +define_u8_enum!( +/// Enum representing the stackerdb message identifier: this is +/// the contract index in the signers contracts (i.e., X in signers-0-X) +MessageSlotID { + /// Block Proposal message from miners + BlockProposal = 0, + /// Block Response message from signers + BlockResponse = 1 +}); + +define_u8_enum!( +/// Enum representing the SignerMessage type prefix +SignerMessageTypePrefix { + /// Block Proposal message from miners + BlockProposal = 0, + /// Block Response message from signers + BlockResponse = 1 +}); + +#[cfg_attr(test, mutants::skip)] +impl MessageSlotID { + /// Return the StackerDB contract corresponding to messages of this type + pub fn stacker_db_contract( + &self, + mainnet: bool, + reward_cycle: u64, + ) -> QualifiedContractIdentifier { + NakamotoSigners::make_signers_db_contract_id(reward_cycle, self.to_u32(), mainnet) + } + + /// Return the u32 identifier for the message slot (used to index the contract that stores it) + pub fn to_u32(self) -> u32 { + self.to_u8().into() + } +} + +#[cfg_attr(test, mutants::skip)] +impl Display for MessageSlotID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}({})", self, self.to_u8()) + } +} + +impl TryFrom for SignerMessageTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown signer message type prefix: {value}")) + }) + } +} + +impl From<&SignerMessage> for SignerMessageTypePrefix { + #[cfg_attr(test, mutants::skip)] + fn from(message: &SignerMessage) -> Self { + match message { + SignerMessage::BlockProposal(_) => SignerMessageTypePrefix::BlockProposal, + SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse, + } + } +} + +/// The messages being sent through the stacker db contracts +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub enum SignerMessage { + /// The block proposal from miners for signers to observe and sign + BlockProposal(BlockProposal), + /// The block response from signers for miners to observe + BlockResponse(BlockResponse), +} + +impl Debug for SignerMessage { + #[cfg_attr(test, mutants::skip)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BlockProposal(b) => Debug::fmt(b, f), + Self::BlockResponse(b) => Debug::fmt(b, f), + } + } +} + +impl SignerMessage { + /// Helper function to determine the slot ID for the provided stacker-db writer id + #[cfg_attr(test, mutants::skip)] + pub fn msg_id(&self) -> MessageSlotID { + match self { + Self::BlockProposal(_) => MessageSlotID::BlockProposal, + Self::BlockResponse(_) => MessageSlotID::BlockResponse, + } + } +} + +impl StacksMessageCodec for SignerMessage { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &(SignerMessageTypePrefix::from(self) as u8))?; + match self { + SignerMessage::BlockProposal(block_proposal) => { + write_next(fd, block_proposal)?; + } + SignerMessage::BlockResponse(block_response) => { + write_next(fd, block_response)?; + } + }; + Ok(()) + } + + #[cfg_attr(test, mutants::skip)] + fn consensus_deserialize(fd: &mut R) -> Result { + let type_prefix_byte = read_next::(fd)?; + let type_prefix = SignerMessageTypePrefix::try_from(type_prefix_byte)?; + let message = match type_prefix { + SignerMessageTypePrefix::BlockProposal => { + let block_proposal = read_next::(fd)?; + SignerMessage::BlockProposal(block_proposal) + } + SignerMessageTypePrefix::BlockResponse => { + let block_response = read_next::(fd)?; + SignerMessage::BlockResponse(block_response) + } + }; + Ok(message) + } +} + +/// Work around for the fact that a lot of the structs being desierialized are not defined in messages.rs +pub trait StacksMessageCodecExtensions: Sized { + /// Serialize the struct to the provided writer + fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError>; + /// Deserialize the struct from the provided reader + fn inner_consensus_deserialize(fd: &mut R) -> Result; +} + +define_u8_enum!( +/// Enum representing the reject code type prefix +RejectCodeTypePrefix { + /// The block was rejected due to validation issues + ValidationFailed = 0, + /// The block was rejected due to connectivity issues with the signer + ConnectivityIssues = 1 +}); + +impl TryFrom for RejectCodeTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown reject code type prefix: {value}")) + }) + } +} + +impl From<&RejectCode> for RejectCodeTypePrefix { + fn from(reject_code: &RejectCode) -> Self { + match reject_code { + RejectCode::ValidationFailed(_) => RejectCodeTypePrefix::ValidationFailed, + RejectCode::ConnectivityIssues => RejectCodeTypePrefix::ConnectivityIssues, + } + } +} + +/// This enum is used to supply a `reason_code` for block rejections +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum RejectCode { + /// RPC endpoint Validation failed + ValidationFailed(ValidateRejectCode), + /// The block was rejected due to connectivity issues with the signer + ConnectivityIssues, +} + +define_u8_enum!( +/// Enum representing the BlockResponse type prefix +BlockResponseTypePrefix { + /// An accepted block response + Accepted = 0, + /// A rejected block response + Rejected = 1 +}); + +impl TryFrom for BlockResponseTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown block response type prefix: {value}")) + }) + } +} + +impl From<&BlockResponse> for BlockResponseTypePrefix { + fn from(block_response: &BlockResponse) -> Self { + match block_response { + BlockResponse::Accepted(_) => BlockResponseTypePrefix::Accepted, + BlockResponse::Rejected(_) => BlockResponseTypePrefix::Rejected, + } + } +} + +/// 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((Sha512Trunc256Sum, MessageSignature)), + /// The Nakamoto block was rejected and therefore not signed + Rejected(BlockRejection), +} + +#[cfg_attr(test, mutants::skip)] +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: MessageSignature) -> Self { + Self::Accepted((hash, sig)) + } + + /// Create a new rejected BlockResponse for the provided block signer signature hash and rejection code + pub fn rejected(hash: Sha512Trunc256Sum, reject_code: RejectCode) -> Self { + Self::Rejected(BlockRejection::new(hash, reject_code)) + } +} + +impl StacksMessageCodec for BlockResponse { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &(BlockResponseTypePrefix::from(self) as u8))?; + match self { + BlockResponse::Accepted((hash, sig)) => { + write_next(fd, hash)?; + write_next(fd, sig)?; + } + BlockResponse::Rejected(rejection) => { + write_next(fd, rejection)?; + } + }; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let type_prefix_byte = read_next::(fd)?; + let type_prefix = BlockResponseTypePrefix::try_from(type_prefix_byte)?; + let response = match type_prefix { + BlockResponseTypePrefix::Accepted => { + let hash = read_next::(fd)?; + let sig = read_next::(fd)?; + BlockResponse::Accepted((hash, sig)) + } + BlockResponseTypePrefix::Rejected => { + let rejection = read_next::(fd)?; + BlockResponse::Rejected(rejection) + } + }; + Ok(response) + } +} + +/// 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 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(signer_signature_hash: Sha512Trunc256Sum, reason_code: RejectCode) -> Self { + Self { + reason: reason_code.to_string(), + reason_code, + signer_signature_hash, + } + } +} + +impl StacksMessageCodec for BlockRejection { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &self.reason.as_bytes().to_vec())?; + write_next(fd, &self.reason_code)?; + write_next(fd, &self.signer_signature_hash)?; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let reason_bytes = read_next::, _>(fd)?; + let reason = String::from_utf8(reason_bytes).map_err(|e| { + CodecError::DeserializeError(format!("Failed to decode reason string: {:?}", &e)) + })?; + let reason_code = read_next::(fd)?; + let signer_signature_hash = read_next::(fd)?; + Ok(Self { + reason, + reason_code, + signer_signature_hash, + }) + } +} + +impl From for BlockRejection { + fn from(reject: BlockValidateReject) -> Self { + Self { + reason: reject.reason, + reason_code: RejectCode::ValidationFailed(reject.reason_code), + signer_signature_hash: reject.signer_signature_hash, + } + } +} + +impl StacksMessageCodec for RejectCode { + fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &(RejectCodeTypePrefix::from(self) as u8))?; + // Do not do a single match here as we may add other variants in the future and don't want to miss adding it + match self { + RejectCode::ValidationFailed(code) => write_next(fd, &(*code as u8))?, + RejectCode::ConnectivityIssues => { + // No additional data to serialize / deserialize + } + }; + Ok(()) + } + + fn consensus_deserialize(fd: &mut R) -> Result { + let type_prefix_byte = read_next::(fd)?; + let type_prefix = RejectCodeTypePrefix::try_from(type_prefix_byte)?; + let code = match type_prefix { + RejectCodeTypePrefix::ValidationFailed => RejectCode::ValidationFailed( + ValidateRejectCode::try_from(read_next::(fd)?).map_err(|e| { + CodecError::DeserializeError(format!( + "Failed to decode validation reject code: {:?}", + &e + )) + })?, + ), + RejectCodeTypePrefix::ConnectivityIssues => RejectCode::ConnectivityIssues, + }; + Ok(code) + } +} + +#[cfg_attr(test, mutants::skip)] +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::ConnectivityIssues => write!( + f, + "The block was rejected due to connectivity issues with the signer." + ), + } + } +} + +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())) + } +} + +#[cfg(test)] +mod test { + use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader; + use blockstack_lib::chainstate::stacks::{ + ThresholdSignature, TransactionAnchorMode, TransactionAuth, TransactionPayload, + TransactionPostConditionMode, TransactionSmartContract, TransactionVersion, + }; + use blockstack_lib::util_lib::strings::StacksString; + use clarity::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; + use clarity::util::hash::MerkleTree; + use clarity::util::secp256k1::MessageSignature; + use rand::{thread_rng, Rng, RngCore}; + use rand_core::OsRng; + use stacks_common::bitvec::BitVec; + use stacks_common::consts::CHAIN_ID_TESTNET; + use stacks_common::types::chainstate::StacksPrivateKey; + + use super::{StacksMessageCodecExtensions, *}; + + #[test] + fn signer_slots_count_is_sane() { + let slot_identifiers_len = MessageSlotID::ALL.len(); + assert!( + SIGNER_SLOTS_PER_USER as usize >= slot_identifiers_len, + "stacks_common::SIGNER_SLOTS_PER_USER ({}) must be >= slot identifiers ({})", + SIGNER_SLOTS_PER_USER, + slot_identifiers_len, + ); + } + + #[test] + fn serde_reject_code() { + let code = RejectCode::ValidationFailed(ValidateRejectCode::InvalidBlock); + let serialized_code = code.serialize_to_vec(); + let deserialized_code = read_next::(&mut &serialized_code[..]) + .expect("Failed to deserialize RejectCode"); + assert_eq!(code, deserialized_code); + + let code = RejectCode::ConnectivityIssues; + let serialized_code = code.serialize_to_vec(); + let deserialized_code = read_next::(&mut &serialized_code[..]) + .expect("Failed to deserialize RejectCode"); + assert_eq!(code, deserialized_code); + } + + #[test] + fn serde_block_rejection() { + let rejection = BlockRejection::new( + Sha512Trunc256Sum([0u8; 32]), + RejectCode::ValidationFailed(ValidateRejectCode::InvalidBlock), + ); + let serialized_rejection = rejection.serialize_to_vec(); + let deserialized_rejection = read_next::(&mut &serialized_rejection[..]) + .expect("Failed to deserialize BlockRejection"); + assert_eq!(rejection, deserialized_rejection); + + let rejection = + BlockRejection::new(Sha512Trunc256Sum([1u8; 32]), RejectCode::ConnectivityIssues); + let serialized_rejection = rejection.serialize_to_vec(); + let deserialized_rejection = read_next::(&mut &serialized_rejection[..]) + .expect("Failed to deserialize BlockRejection"); + assert_eq!(rejection, deserialized_rejection); + } + + #[test] + fn serde_block_response() { + let response = + BlockResponse::Accepted((Sha512Trunc256Sum([0u8; 32]), MessageSignature::empty())); + let serialized_response = response.serialize_to_vec(); + let deserialized_response = read_next::(&mut &serialized_response[..]) + .expect("Failed to deserialize BlockResponse"); + assert_eq!(response, deserialized_response); + + let response = BlockResponse::Rejected(BlockRejection::new( + Sha512Trunc256Sum([1u8; 32]), + RejectCode::ValidationFailed(ValidateRejectCode::InvalidBlock), + )); + let serialized_response = response.serialize_to_vec(); + let deserialized_response = read_next::(&mut &serialized_response[..]) + .expect("Failed to deserialize BlockResponse"); + assert_eq!(response, deserialized_response); + } + + #[test] + fn serde_signer_message() { + let signer_message = SignerMessage::BlockResponse(BlockResponse::Accepted(( + Sha512Trunc256Sum([2u8; 32]), + MessageSignature::empty(), + ))); + let serialized_signer_message = signer_message.serialize_to_vec(); + let deserialized_signer_message = + read_next::(&mut &serialized_signer_message[..]) + .expect("Failed to deserialize SignerMessage"); + assert_eq!(signer_message, deserialized_signer_message); + + let header = NakamotoBlockHeader::empty(); + let mut block = NakamotoBlock { + header, + txs: vec![], + }; + let tx_merkle_root = { + let txid_vecs = block + .txs + .iter() + .map(|tx| tx.txid().as_bytes().to_vec()) + .collect(); + + MerkleTree::::new(&txid_vecs).root() + }; + block.header.tx_merkle_root = tx_merkle_root; + + let block_proposal = BlockProposal { + block, + burn_height: thread_rng().next_u64(), + reward_cycle: thread_rng().next_u64(), + }; + let signer_message = SignerMessage::BlockProposal(block_proposal); + let serialized_signer_message = signer_message.serialize_to_vec(); + let deserialized_signer_message = + read_next::(&mut &serialized_signer_message[..]) + .expect("Failed to deserialize SignerMessage"); + assert_eq!(signer_message, deserialized_signer_message); + } +} diff --git a/libsigner/src/v0/mod.rs b/libsigner/src/v0/mod.rs new file mode 100644 index 000000000..703acb85f --- /dev/null +++ b/libsigner/src/v0/mod.rs @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// Messages for the v0 signer +pub mod messages; diff --git a/libsigner/src/messages.rs b/libsigner/src/v1/messages.rs similarity index 92% rename from libsigner/src/messages.rs rename to libsigner/src/v1/messages.rs index 388207a77..a84ba98c6 100644 --- a/libsigner/src/messages.rs +++ b/libsigner/src/v1/messages.rs @@ -100,14 +100,22 @@ MessageSlotID { EncryptedSignerState = 13 }); -define_u8_enum!(SignerMessageTypePrefix { +define_u8_enum!( +/// Enum representing the signer message type prefix +SignerMessageTypePrefix { + /// A block response message BlockResponse = 0, + /// A wsts packet message Packet = 1, + /// A list of transactions that a signer cares about Transactions = 2, + /// The results of a successful DKG DkgResults = 3, + /// The encrypted state of the signer to be persisted EncryptedSignerState = 4 }); +#[cfg_attr(test, mutants::skip)] impl MessageSlotID { /// Return the StackerDB contract corresponding to messages of this type pub fn stacker_db_contract( @@ -152,16 +160,28 @@ impl From<&SignerMessage> for SignerMessageTypePrefix { } } -define_u8_enum!(MessageTypePrefix { +define_u8_enum!( +/// Enum representing the message type prefix +MessageTypePrefix { + /// DkgBegin message DkgBegin = 0, + /// DkgPrivateBegin message DkgPrivateBegin = 1, + /// DkgEndBegin message DkgEndBegin = 2, + /// DkgEnd message DkgEnd = 3, + /// DkgPublicShares message DkgPublicShares = 4, + /// DkgPrivateShares message DkgPrivateShares = 5, + /// NonceRequest message NonceRequest = 6, + /// NonceResponse message NonceResponse = 7, + /// SignatureShareRequest message SignatureShareRequest = 8, + /// SignatureShareResponse message SignatureShareResponse = 9 }); @@ -191,13 +211,22 @@ impl TryFrom for MessageTypePrefix { } } -define_u8_enum!(RejectCodeTypePrefix{ +define_u8_enum!( +/// Enum representing the reject code type prefix +RejectCodeTypePrefix { + /// Validation failed ValidationFailed = 0, + /// Signed rejection SignedRejection = 1, + /// Insufficient signers InsufficientSigners = 2, + /// Missing transactions MissingTransactions = 3, + /// Connectivity issues ConnectivityIssues = 4, + /// Nonce timeout NonceTimeout = 5, + /// Aggregator error AggregatorError = 6 }); @@ -417,7 +446,9 @@ impl StacksMessageCodec for SignerMessage { /// Work around for the fact that a lot of the structs being desierialized are not defined in messages.rs pub trait StacksMessageCodecExtensions: Sized { + /// Serialize the struct to the provided writer fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError>; + /// Deserialize the struct from the provided reader fn inner_consensus_deserialize(fd: &mut R) -> Result; } @@ -537,50 +568,87 @@ impl StacksMessageCodecExtensions for HashSet { } } +define_u8_enum!( +/// Enum representing the DKG failure type prefix +DkgFailureTypePrefix { + /// Bad state + BadState = 0, + /// Missing public shares + MissingPublicShares = 1, + /// Bad public shares + BadPublicShares = 2, + /// Missing private shares + MissingPrivateShares = 3, + /// Bad private shares + BadPrivateShares = 4 +}); + +impl TryFrom for DkgFailureTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown DKG failure type prefix: {value}")) + }) + } +} + +impl From<&DkgFailure> for DkgFailureTypePrefix { + fn from(failure: &DkgFailure) -> Self { + match failure { + DkgFailure::BadState => DkgFailureTypePrefix::BadState, + DkgFailure::MissingPublicShares(_) => DkgFailureTypePrefix::MissingPublicShares, + DkgFailure::BadPublicShares(_) => DkgFailureTypePrefix::BadPublicShares, + DkgFailure::MissingPrivateShares(_) => DkgFailureTypePrefix::MissingPrivateShares, + DkgFailure::BadPrivateShares(_) => DkgFailureTypePrefix::BadPrivateShares, + } + } +} + impl StacksMessageCodecExtensions for DkgFailure { fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &(DkgFailureTypePrefix::from(self) as u8))?; match self { - DkgFailure::BadState => write_next(fd, &0u8), + DkgFailure::BadState => { + // No additional data to serialize + } DkgFailure::MissingPublicShares(shares) => { - write_next(fd, &1u8)?; - shares.inner_consensus_serialize(fd) + shares.inner_consensus_serialize(fd)?; } DkgFailure::BadPublicShares(shares) => { - write_next(fd, &2u8)?; - shares.inner_consensus_serialize(fd) + shares.inner_consensus_serialize(fd)?; } DkgFailure::MissingPrivateShares(shares) => { - write_next(fd, &3u8)?; - shares.inner_consensus_serialize(fd) + shares.inner_consensus_serialize(fd)?; } DkgFailure::BadPrivateShares(shares) => { - write_next(fd, &4u8)?; write_next(fd, &(shares.len() as u32))?; for (id, share) in shares { write_next(fd, id)?; share.inner_consensus_serialize(fd)?; } - Ok(()) } } + Ok(()) } + fn inner_consensus_deserialize(fd: &mut R) -> Result { - let failure_type_prefix = read_next::(fd)?; + let failure_type_prefix_byte = read_next::(fd)?; + let failure_type_prefix = DkgFailureTypePrefix::try_from(failure_type_prefix_byte)?; let failure_type = match failure_type_prefix { - 0 => DkgFailure::BadState, - 1 => { + DkgFailureTypePrefix::BadState => DkgFailure::BadState, + DkgFailureTypePrefix::MissingPublicShares => { let set = HashSet::::inner_consensus_deserialize(fd)?; DkgFailure::MissingPublicShares(set) } - 2 => { + DkgFailureTypePrefix::BadPublicShares => { let set = HashSet::::inner_consensus_deserialize(fd)?; DkgFailure::BadPublicShares(set) } - 3 => { + DkgFailureTypePrefix::MissingPrivateShares => { let set = HashSet::::inner_consensus_deserialize(fd)?; DkgFailure::MissingPrivateShares(set) } - 4 => { + DkgFailureTypePrefix::BadPrivateShares => { let mut map = HashMap::new(); let len = read_next::(fd)?; for _ in 0..len { @@ -590,12 +658,6 @@ impl StacksMessageCodecExtensions for DkgFailure { } DkgFailure::BadPrivateShares(map) } - _ => { - return Err(CodecError::DeserializeError(format!( - "Unknown DkgFailure type prefix: {}", - failure_type_prefix - ))) - } }; Ok(failure_type) } @@ -647,34 +709,60 @@ impl StacksMessageCodecExtensions for DkgEndBegin { } } +define_u8_enum!( +/// Enum representing the DKG status type prefix +DkgStatusTypePrefix { + /// Success + Success = 0, + /// Failure + Failure = 1 +}); + +impl TryFrom for DkgStatusTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown DKG status type prefix: {value}")) + }) + } +} + +impl From<&DkgStatus> for DkgStatusTypePrefix { + fn from(status: &DkgStatus) -> Self { + match status { + DkgStatus::Success => DkgStatusTypePrefix::Success, + DkgStatus::Failure(_) => DkgStatusTypePrefix::Failure, + } + } +} + impl StacksMessageCodecExtensions for DkgEnd { fn inner_consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { write_next(fd, &self.dkg_id)?; write_next(fd, &self.signer_id)?; + write_next(fd, &(DkgStatusTypePrefix::from(&self.status) as u8))?; match &self.status { - DkgStatus::Success => write_next(fd, &0u8), + DkgStatus::Success => { + // No additional data to serialize + } DkgStatus::Failure(failure) => { - write_next(fd, &1u8)?; - failure.inner_consensus_serialize(fd) + failure.inner_consensus_serialize(fd)?; } } + Ok(()) } + fn inner_consensus_deserialize(fd: &mut R) -> Result { let dkg_id = read_next::(fd)?; let signer_id = read_next::(fd)?; - let status_type_prefix = read_next::(fd)?; + let status_type_prefix_byte = read_next::(fd)?; + let status_type_prefix = DkgStatusTypePrefix::try_from(status_type_prefix_byte)?; let status = match status_type_prefix { - 0 => DkgStatus::Success, - 1 => { + DkgStatusTypePrefix::Success => DkgStatus::Success, + DkgStatusTypePrefix::Failure => { let failure = DkgFailure::inner_consensus_deserialize(fd)?; DkgStatus::Failure(failure) } - _ => { - return Err(CodecError::DeserializeError(format!( - "Unknown DKG status type prefix: {}", - status_type_prefix - ))) - } }; Ok(DkgEnd { dkg_id, @@ -1035,6 +1123,33 @@ impl StacksMessageCodecExtensions for Packet { } } +define_u8_enum!( +/// Enum representing the block response type prefix +BlockResponseTypePrefix { + /// Accepted + Accepted = 0, + /// Rejected + Rejected = 1 +}); + +impl TryFrom for BlockResponseTypePrefix { + type Error = CodecError; + fn try_from(value: u8) -> Result { + Self::from_u8(value).ok_or_else(|| { + CodecError::DeserializeError(format!("Unknown block response type prefix: {value}")) + }) + } +} + +impl From<&BlockResponse> for BlockResponseTypePrefix { + fn from(block_response: &BlockResponse) -> Self { + match block_response { + BlockResponse::Accepted(_) => BlockResponseTypePrefix::Accepted, + BlockResponse::Rejected(_) => BlockResponseTypePrefix::Rejected, + } + } +} + /// 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)] @@ -1083,14 +1198,13 @@ impl BlockResponse { impl StacksMessageCodec for BlockResponse { fn consensus_serialize(&self, fd: &mut W) -> Result<(), CodecError> { + write_next(fd, &(BlockResponseTypePrefix::from(self) as u8))?; match self { BlockResponse::Accepted((hash, sig)) => { - write_next(fd, &0u8)?; write_next(fd, hash)?; write_next(fd, sig)?; } BlockResponse::Rejected(rejection) => { - write_next(fd, &1u8)?; write_next(fd, rejection)?; } }; @@ -1098,27 +1212,23 @@ impl StacksMessageCodec for BlockResponse { } fn consensus_deserialize(fd: &mut R) -> Result { - let type_prefix = read_next::(fd)?; + let type_prefix_byte = read_next::(fd)?; + let type_prefix = BlockResponseTypePrefix::try_from(type_prefix_byte)?; let response = match type_prefix { - 0 => { + BlockResponseTypePrefix::Accepted => { let hash = read_next::(fd)?; let sig = read_next::(fd)?; BlockResponse::Accepted((hash, sig)) } - 1 => { + BlockResponseTypePrefix::Rejected => { let rejection = read_next::(fd)?; BlockResponse::Rejected(rejection) } - _ => { - return Err(CodecError::DeserializeError(format!( - "Unknown block response type prefix: {}", - type_prefix - ))) - } }; Ok(response) } } + /// A rejection response from a signer for a proposed block #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BlockRejection { diff --git a/libsigner/src/v1/mod.rs b/libsigner/src/v1/mod.rs new file mode 100644 index 000000000..e5a691efb --- /dev/null +++ b/libsigner/src/v1/mod.rs @@ -0,0 +1,17 @@ +// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/// Messages for the v1 signer +pub mod messages; diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index e1edf3aec..f23679b09 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -17,7 +17,8 @@ use blockstack_lib::chainstate::stacks::StacksTransaction; use blockstack_lib::net::api::poststackerdbchunk::StackerDBErrorCodes; use hashbrown::HashMap; -use libsigner::{MessageSlotID, SignerMessage, SignerSession, StackerDBSession}; +use libsigner::v1::messages::{MessageSlotID, SignerMessage}; +use libsigner::{SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use slog::{slog_debug, slog_error, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs index 9cef3da9a..6f07bb362 100644 --- a/stacks-signer/src/client/stacks_client.rs +++ b/stacks-signer/src/client/stacks_client.rs @@ -732,18 +732,13 @@ mod tests { use blockstack_lib::chainstate::stacks::boot::{ NakamotoSignerEntry, PoxStartCycleInfo, RewardSet, }; - use blockstack_lib::chainstate::stacks::ThresholdSignature; use clarity::vm::types::{ ListData, ListTypeData, ResponseData, SequenceData, TupleData, TupleTypeSignature, TypeSignature, }; use rand::thread_rng; use rand_core::RngCore; - use stacks_common::bitvec::BitVec; use stacks_common::consts::{CHAIN_ID_TESTNET, SIGNER_SLOTS_PER_USER}; - use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; - use stacks_common::util::hash::Sha512Trunc256Sum; - use stacks_common::util::secp256k1::MessageSignature; use wsts::curve::scalar::Scalar; use super::*; @@ -1229,18 +1224,7 @@ mod tests { #[test] fn submit_block_for_validation_should_succeed() { let mock = MockServerClient::new(); - let header = NakamotoBlockHeader { - version: 1, - chain_length: 2, - burn_spent: 3, - consensus_hash: ConsensusHash([0x04; 20]), - parent_block_id: StacksBlockId([0x05; 32]), - tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), - state_index_root: TrieHash([0x07; 32]), - miner_signature: MessageSignature::empty(), - signer_signature: ThresholdSignature::empty(), - signer_bitvec: BitVec::zeros(1).unwrap(), - }; + let header = NakamotoBlockHeader::empty(); let block = NakamotoBlock { header, txs: vec![], @@ -1253,18 +1237,7 @@ mod tests { #[test] fn submit_block_for_validation_should_fail() { let mock = MockServerClient::new(); - let header = NakamotoBlockHeader { - version: 1, - chain_length: 2, - burn_spent: 3, - consensus_hash: ConsensusHash([0x04; 20]), - parent_block_id: StacksBlockId([0x05; 32]), - tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), - state_index_root: TrieHash([0x07; 32]), - miner_signature: MessageSignature::empty(), - signer_signature: ThresholdSignature::empty(), - signer_bitvec: BitVec::zeros(1).unwrap(), - }; + let header = NakamotoBlockHeader::empty(); let block = NakamotoBlock { header, txs: vec![], diff --git a/stacks-signer/src/lib.rs b/stacks-signer/src/lib.rs index 2a9e15675..0e8a6b10b 100644 --- a/stacks-signer/src/lib.rs +++ b/stacks-signer/src/lib.rs @@ -34,11 +34,10 @@ pub mod runloop; pub mod v0; /// The v1 implementation of the singer. This includes WSTS support pub mod v1; - use std::fmt::{Debug, Display}; use std::sync::mpsc::Sender; -use libsigner::SignerEvent; +use libsigner::{SignerEvent, SignerEventTrait}; use wsts::state_machine::OperationResult; use crate::client::StacksClient; @@ -46,7 +45,7 @@ use crate::config::SignerConfig; use crate::runloop::RunLoopCommand; /// A trait which provides a common `Signer` interface for `v1` and `v2` -pub trait Signer: Debug + Display { +pub trait Signer: Debug + Display { /// Create a new `Signer` instance fn new(config: SignerConfig) -> Self; /// Update the `Signer` instance's next reward cycle data with the latest `SignerConfig` @@ -57,7 +56,7 @@ pub trait Signer: Debug + Display { fn process_event( &mut self, stacks_client: &StacksClient, - event: Option<&SignerEvent>, + event: Option<&SignerEvent>, res: Sender>, current_reward_cycle: u64, ); diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index f7b5a24e7..75514fd2e 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -27,34 +27,24 @@ extern crate serde_json; extern crate toml; use std::io::{self, Write}; -use std::path::PathBuf; -use std::sync::mpsc::{channel, Receiver, Sender}; use blockstack_lib::util_lib::signed_structured_data::pox4::make_pox_4_signer_key_signature; use clap::Parser; use clarity::vm::types::QualifiedContractIdentifier; -use libsigner::{RunningSigner, Signer, SignerEventReceiver, SignerSession, StackerDBSession}; +use libsigner::{SignerSession, StackerDBSession}; use libstackerdb::StackerDBChunkData; -use slog::{slog_debug, slog_info}; +use slog::slog_debug; +use stacks_common::debug; use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; -use stacks_common::{debug, info}; use stacks_signer::cli::{ Cli, Command, GenerateStackingSignatureArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunSignerArgs, StackerDBArgs, }; use stacks_signer::config::GlobalConfig; -use stacks_signer::runloop::{RunLoop, RunLoopCommand}; use stacks_signer::v1; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; -use wsts::state_machine::OperationResult; - -struct SpawnedSigner { - running_signer: RunningSigner>, - _cmd_send: Sender, - _res_recv: Receiver>, -} /// Create a new stacker db session fn stackerdb_session(host: &str, contract: QualifiedContractIdentifier) -> StackerDBSession { @@ -78,33 +68,6 @@ fn write_chunk_to_stdout(chunk_opt: Option>) { } } -// Spawn a running signer and return its handle, command sender, and result receiver -fn spawn_running_signer(path: &PathBuf) -> SpawnedSigner { - let config = GlobalConfig::try_from(path).unwrap(); - let endpoint = config.endpoint; - info!("Starting signer with config: {}", config); - let (_cmd_send, cmd_recv) = channel(); - let (res_send, _res_recv) = channel(); - let ev = SignerEventReceiver::new(config.network.is_mainnet()); - #[cfg(feature = "monitoring_prom")] - { - stacks_signer::monitoring::start_serving_monitoring_metrics(config.clone()).ok(); - } - let runloop = RunLoop::new(config); - let mut signer: Signer< - RunLoopCommand, - Vec, - RunLoop, - SignerEventReceiver, - > = Signer::new(runloop, ev, cmd_recv, res_send); - let running_signer = signer.spawn(endpoint).unwrap(); - SpawnedSigner { - running_signer, - _cmd_send, - _res_recv, - } -} - fn handle_get_chunk(args: GetChunkArgs) { debug!("Getting chunk..."); let mut session = stackerdb_session(&args.db_args.host, args.db_args.contract); @@ -139,10 +102,11 @@ fn handle_put_chunk(args: PutChunkArgs) { fn handle_run(args: RunSignerArgs) { debug!("Running signer..."); - let spawned_signer = spawn_running_signer(&args.config); + let config = GlobalConfig::try_from(&args.config).unwrap(); + let spawned_signer = v1::SpawnedSigner::from(config); println!("Signer spawned successfully. Waiting for messages to process..."); // Wait for the spawned signer to stop (will only occur if an error occurs) - let _ = spawned_signer.running_signer.join(); + let _ = spawned_signer.join(); } fn handle_generate_stacking_signature( diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index e38e846cd..ad25b4fc4 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -1,4 +1,5 @@ use std::collections::VecDeque; +use std::fmt::Debug; // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation // Copyright (C) 2020-2024 Stacks Open Internet Foundation // @@ -20,8 +21,9 @@ use std::time::Duration; use blockstack_lib::burnchains::PoxConstants; use blockstack_lib::chainstate::stacks::boot::SIGNERS_NAME; use blockstack_lib::util_lib::boot::boot_code_id; +use clarity::codec::StacksMessageCodec; use hashbrown::HashMap; -use libsigner::{BlockProposalSigners, SignerEntries, SignerEvent, SignerRunLoop}; +use libsigner::{BlockProposal, SignerEntries, SignerEvent, SignerRunLoop}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::{debug, error, info, warn}; @@ -40,7 +42,7 @@ pub enum SignerCommand { /// Sign a message Sign { /// The block to sign over - block_proposal: BlockProposalSigners, + block_proposal: BlockProposal, /// Whether to make a taproot signature is_taproot: bool, /// Taproot merkle root @@ -118,7 +120,11 @@ impl RewardCycleInfo { } /// The runloop for the stacks signer -pub struct RunLoop { +pub struct RunLoop +where + Signer: SignerTrait, + T: StacksMessageCodec + Clone + Send + Debug, +{ /// Configuration info pub config: GlobalConfig, /// The stacks node client @@ -132,9 +138,11 @@ pub struct RunLoop { pub commands: VecDeque, /// The current reward cycle info. Only None if the runloop is uninitialized pub current_reward_cycle_info: Option, + /// Phantom data for the message codec + _phantom_data: std::marker::PhantomData, } -impl RunLoop { +impl, T: StacksMessageCodec + Clone + Send + Debug> RunLoop { /// Create a new signer runloop from the provided configuration pub fn new(config: GlobalConfig) -> Self { let stacks_client = StacksClient::from(&config); @@ -145,6 +153,7 @@ impl RunLoop { state: State::Uninitialized, commands: VecDeque::new(), current_reward_cycle_info: None, + _phantom_data: std::marker::PhantomData, } } /// Get the registered signers for a specific reward cycle @@ -357,7 +366,9 @@ impl RunLoop { } } -impl SignerRunLoop, RunLoopCommand> for RunLoop { +impl, T: StacksMessageCodec + Clone + Send + Debug> + SignerRunLoop, RunLoopCommand, T> for RunLoop +{ fn set_event_timeout(&mut self, timeout: Duration) { self.config.event_timeout = timeout; } @@ -368,7 +379,7 @@ impl SignerRunLoop, RunLoopCommand> fo fn run_one_pass( &mut self, - event: Option, + event: Option>, cmd: Option, res: Sender>, ) -> Option> { diff --git a/stacks-signer/src/v1/mod.rs b/stacks-signer/src/v1/mod.rs index 88d5afb73..7c2477cf2 100644 --- a/stacks-signer/src/v1/mod.rs +++ b/stacks-signer/src/v1/mod.rs @@ -20,3 +20,72 @@ pub mod coordinator; pub mod signer; /// The state module for the signer pub mod signerdb; + +use std::sync::mpsc::{channel, Receiver, Sender}; + +use libsigner::v1::messages::SignerMessage; +use libsigner::SignerEventReceiver; +use slog::slog_info; +use stacks_common::info; +use wsts::state_machine::OperationResult; + +use crate::config::GlobalConfig; +use crate::runloop::{RunLoop, RunLoopCommand}; +use crate::v1::signer::Signer; + +/// The signer type for the v1 signer +pub type RunningSigner = libsigner::RunningSigner< + SignerEventReceiver, + Vec, + SignerMessage, +>; + +/// The spawned signer type for the v1 signer +pub struct SpawnedSigner { + /// The underlying running signer thread handle + running_signer: RunningSigner, + /// The command sender for interacting with the running signer + pub cmd_send: Sender, + /// The result receiver for interacting with the running signer + pub res_recv: Receiver>, +} + +impl From for SpawnedSigner { + fn from(config: GlobalConfig) -> Self { + let endpoint = config.endpoint; + info!("Starting signer with config: {}", config); + let (cmd_send, cmd_recv) = channel(); + let (res_send, res_recv) = channel(); + let ev = SignerEventReceiver::new(config.network.is_mainnet()); + #[cfg(feature = "monitoring_prom")] + { + crate::monitoring::start_serving_monitoring_metrics(config.clone()).ok(); + } + let runloop = RunLoop::new(config); + let mut signer: libsigner::Signer< + RunLoopCommand, + Vec, + RunLoop, + SignerEventReceiver, + SignerMessage, + > = libsigner::Signer::new(runloop, ev, cmd_recv, res_send); + let running_signer = signer.spawn(endpoint).unwrap(); + SpawnedSigner { + running_signer, + cmd_send, + res_recv, + } + } +} + +impl SpawnedSigner { + /// Stop the signer thread and return the final state + pub fn stop(self) -> Option> { + self.running_signer.stop() + } + + /// Wait for the signer to terminate, and get the final state. WARNING: This will hang forever if the event receiver stop signal was never sent/no error occurred. + pub fn join(self) -> Option> { + self.running_signer.join() + } +} diff --git a/stacks-signer/src/v1/signer.rs b/stacks-signer/src/v1/signer.rs index b04f7b81a..fd22490a0 100644 --- a/stacks-signer/src/v1/signer.rs +++ b/stacks-signer/src/v1/signer.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::collections::VecDeque; +use std::fmt::Debug; use std::path::PathBuf; use std::sync::mpsc::Sender; use std::time::Instant; @@ -26,10 +27,10 @@ use blockstack_lib::chainstate::stacks::StacksTransaction; use blockstack_lib::net::api::postblock_proposal::BlockValidateResponse; use blockstack_lib::util_lib::db::Error as DBError; use hashbrown::HashSet; -use libsigner::{ - BlockProposalSigners, BlockRejection, BlockResponse, MessageSlotID, RejectCode, SignerEvent, - SignerMessage, +use libsigner::v1::messages::{ + BlockRejection, BlockResponse, MessageSlotID, RejectCode, SignerMessage, }; +use libsigner::{BlockProposal, SignerEvent}; use rand_core::OsRng; use serde_derive::{Deserialize, Serialize}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; @@ -78,8 +79,8 @@ pub struct BlockInfo { pub signed_over: bool, } -impl From for BlockInfo { - fn from(value: BlockProposalSigners) -> Self { +impl From for BlockInfo { + fn from(value: BlockProposal) -> Self { Self { block: value.block, burn_block_height: value.burn_height, @@ -93,10 +94,7 @@ impl From for BlockInfo { } impl BlockInfo { /// Create a new BlockInfo with an associated nonce request packet - pub fn new_with_request( - block_proposal: BlockProposalSigners, - nonce_request: NonceRequest, - ) -> Self { + pub fn new_with_request(block_proposal: BlockProposal, nonce_request: NonceRequest) -> Self { let mut block_info = BlockInfo::from(block_proposal); block_info.nonce_request = Some(nonce_request); block_info.signed_over = true; @@ -185,7 +183,7 @@ impl std::fmt::Display for Signer { } } -impl SignerTrait for Signer { +impl SignerTrait for Signer { /// Create a new signer from the given configuration fn new(config: SignerConfig) -> Self { Self::from(config) @@ -209,7 +207,7 @@ impl SignerTrait for Signer { fn process_event( &mut self, stacks_client: &StacksClient, - event: Option<&SignerEvent>, + event: Option<&SignerEvent>, res: Sender>, current_reward_cycle: u64, ) { @@ -262,11 +260,9 @@ impl SignerTrait for Signer { self.handle_signer_messages(stacks_client, res, messages, current_reward_cycle); } Some(SignerEvent::MinerMessages(messages, miner_key)) => { - if let Some(miner_key) = miner_key { - let miner_key = PublicKey::try_from(miner_key.to_bytes_compressed().as_slice()) - .expect("FATAL: could not convert from StacksPublicKey to PublicKey"); - self.miner_key = Some(miner_key); - }; + let miner_key = PublicKey::try_from(miner_key.to_bytes_compressed().as_slice()) + .expect("FATAL: could not convert from StacksPublicKey to PublicKey"); + self.miner_key = Some(miner_key); if current_reward_cycle != self.reward_cycle { // There is not point in processing blocks if we are not the current reward cycle (we can never actually contribute to signing these blocks) debug!("{self}: Received a proposed block, but this signer's reward cycle is not the current one ({current_reward_cycle}). Ignoring..."); @@ -913,7 +909,7 @@ impl Signer { nonce_request: &mut NonceRequest, ) -> Option { let Some(block_proposal) = - BlockProposalSigners::consensus_deserialize(&mut nonce_request.message.as_slice()).ok() + BlockProposal::consensus_deserialize(&mut nonce_request.message.as_slice()).ok() else { // We currently reject anything that is not a valid block proposal warn!("{self}: Received a nonce request for an unknown message stream. Reject it.",); diff --git a/stacks-signer/src/v1/signerdb.rs b/stacks-signer/src/v1/signerdb.rs index 0948fbe16..139bed048 100644 --- a/stacks-signer/src/v1/signerdb.rs +++ b/stacks-signer/src/v1/signerdb.rs @@ -183,11 +183,7 @@ mod tests { use blockstack_lib::chainstate::nakamoto::{ NakamotoBlock, NakamotoBlockHeader, NakamotoBlockVote, }; - use blockstack_lib::chainstate::stacks::ThresholdSignature; - use libsigner::BlockProposalSigners; - use stacks_common::bitvec::BitVec; - use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash}; - use stacks_common::util::secp256k1::MessageSignature; + use libsigner::BlockProposal; use super::*; @@ -198,25 +194,14 @@ mod tests { } fn create_block_override( - overrides: impl FnOnce(&mut BlockProposalSigners), - ) -> (BlockInfo, BlockProposalSigners) { - let header = NakamotoBlockHeader { - version: 1, - chain_length: 2, - burn_spent: 3, - consensus_hash: ConsensusHash([0x04; 20]), - parent_block_id: StacksBlockId([0x05; 32]), - tx_merkle_root: Sha512Trunc256Sum([0x06; 32]), - state_index_root: TrieHash([0x07; 32]), - miner_signature: MessageSignature::empty(), - signer_signature: ThresholdSignature::empty(), - signer_bitvec: BitVec::zeros(1).unwrap(), - }; + overrides: impl FnOnce(&mut BlockProposal), + ) -> (BlockInfo, BlockProposal) { + let header = NakamotoBlockHeader::empty(); let block = NakamotoBlock { header, txs: vec![], }; - let mut block_proposal = BlockProposalSigners { + let mut block_proposal = BlockProposal { block, burn_height: 7, reward_cycle: 42, @@ -225,7 +210,7 @@ mod tests { (BlockInfo::from(block_proposal.clone()), block_proposal) } - fn create_block() -> (BlockInfo, BlockProposalSigners) { + fn create_block() -> (BlockInfo, BlockProposal) { create_block_override(|_| {}) } diff --git a/stackslib/src/net/api/getstxtransfercost.rs b/stackslib/src/net/api/getstxtransfercost.rs index 22e4b4826..b8801e7d7 100644 --- a/stackslib/src/net/api/getstxtransfercost.rs +++ b/stackslib/src/net/api/getstxtransfercost.rs @@ -170,7 +170,7 @@ impl HttpResponse for RPCGetStxTransferCostRequestHandler { impl StacksHttpRequest { pub fn new_get_stx_transfer_cost(host: PeerHost) -> StacksHttpRequest { - let mut contents = HttpRequestContents::new(); + let contents = HttpRequestContents::new(); StacksHttpRequest::new_for_peer(host, "GET".into(), "/v2/fees/transfer".into(), contents) .expect("FATAL: failed to construct request from infallible data") } diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 4b793d010..d6edd7996 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -21,7 +21,7 @@ use std::time::{Duration, Instant}; use clarity::vm::clarity::ClarityConnection; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use hashbrown::HashSet; -use libsigner::{MessageSlotID, SignerMessage}; +use libsigner::v1::messages::{MessageSlotID, SignerMessage}; use stacks::burnchains::Burnchain; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; diff --git a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs index 0bdd01dc7..466795891 100644 --- a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs +++ b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs @@ -17,10 +17,8 @@ use std::sync::mpsc::Receiver; use std::time::{Duration, Instant}; use hashbrown::{HashMap, HashSet}; -use libsigner::{ - BlockProposalSigners, MessageSlotID, SignerEntries, SignerEvent, SignerMessage, SignerSession, - StackerDBSession, -}; +use libsigner::v1::messages::{MessageSlotID, SignerMessage}; +use libsigner::{BlockProposal, SignerEntries, SignerEvent, SignerSession, StackerDBSession}; use stacks::burnchains::Burnchain; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::BlockSnapshot; @@ -384,7 +382,7 @@ impl SignCoordinator { self.coordinator.current_sign_id = sign_id; self.coordinator.current_sign_iter_id = sign_iter_id; - let proposal_msg = BlockProposalSigners { + let proposal_msg = BlockProposal { block: block.clone(), burn_height: burn_block_height, reward_cycle: reward_cycle_id, diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 5e4c8a68b..3dea6c6d2 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -26,7 +26,8 @@ use clarity::vm::costs::ExecutionCost; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use http_types::headers::AUTHORIZATION; use lazy_static::lazy_static; -use libsigner::{BlockProposalSigners, SignerMessage, SignerSession, StackerDBSession}; +use libsigner::v1::messages::SignerMessage; +use libsigner::{BlockProposal, SignerSession, StackerDBSession}; use rand::RngCore; use stacks::burnchains::{MagicBytes, Txid}; use stacks::chainstate::burn::db::sortdb::SortitionDB; @@ -319,7 +320,7 @@ pub fn get_latest_block_proposal( panic!("Expected a nonce request. Got {:?}", packet.msg); }; let block_proposal = - BlockProposalSigners::consensus_deserialize(&mut nonce_request.message.as_slice()) + BlockProposal::consensus_deserialize(&mut nonce_request.message.as_slice()) .expect("Failed to deserialize block proposal"); block_proposal.block }; diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 7f8845dc7..08cb254ec 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -1,17 +1,14 @@ use std::collections::HashSet; use std::net::ToSocketAddrs; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{env, thread}; use clarity::boot_util::boot_code_id; use clarity::vm::Value; -use libsigner::{ - BlockProposalSigners, BlockResponse, MessageSlotID, RejectCode, RunningSigner, Signer, - SignerEntries, SignerEventReceiver, SignerMessage, -}; +use libsigner::v1::messages::{BlockResponse, MessageSlotID, RejectCode, SignerMessage}; +use libsigner::{BlockProposal, SignerEntries}; use rand::thread_rng; use rand_core::RngCore; use stacks::burnchains::Txid; @@ -43,8 +40,8 @@ use stacks_common::util::secp256k1::MessageSignature; use stacks_signer::client::{SignerSlotID, StackerDB, StacksClient}; use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network}; use stacks_signer::runloop::{RunLoopCommand, SignerCommand}; -use stacks_signer::v1; use stacks_signer::v1::coordinator::CoordinatorSelector; +use stacks_signer::v1::SpawnedSigner; use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; use wsts::curve::point::Point; @@ -85,12 +82,8 @@ struct RunningNodes { struct SignerTest { // The stx and bitcoin nodes and their run loops pub running_nodes: RunningNodes, - // The channels for sending commands to the signers - pub signer_cmd_senders: Vec>, - // The channels for receiving results from the signers - pub result_receivers: Vec>>, - // The running signer and its threads - pub running_signers: Vec>>, + // The spawned signers and their threads + pub spawned_signers: Vec, // the private keys of the signers pub signer_stacks_private_keys: Vec, // link to the stacks node @@ -128,21 +121,15 @@ impl SignerTest { Some(9000), ); - let mut running_signers = Vec::new(); - let mut signer_cmd_senders = Vec::new(); - let mut result_receivers = Vec::new(); - for i in 0..num_signers { - let (cmd_send, cmd_recv) = channel(); - let (res_send, res_recv) = channel(); - info!("spawn signer"); - running_signers.push(spawn_signer( - &signer_configs[i as usize], - cmd_recv, - res_send, - )); - signer_cmd_senders.push(cmd_send); - result_receivers.push(res_recv); - } + let spawned_signers: Vec<_> = (0..num_signers) + .into_iter() + .map(|i| { + info!("spawning signer"); + let signer_config = + SignerConfig::load_from_str(&signer_configs[i as usize]).unwrap(); + SpawnedSigner::from(signer_config) + }) + .collect(); // Setup the nodes and deploy the contract to it let node = setup_stx_btc_node(naka_conf, &signer_stacks_private_keys, &signer_configs); @@ -151,9 +138,7 @@ impl SignerTest { Self { running_nodes: node, - result_receivers, - signer_cmd_senders, - running_signers, + spawned_signers, signer_stacks_private_keys, stacks_client, run_stamp, @@ -431,10 +416,11 @@ impl SignerTest { debug!("Waiting for DKG..."); let mut key = Point::default(); let dkg_now = Instant::now(); - for recv in self.result_receivers.iter() { + for signer in self.spawned_signers.iter() { let mut aggregate_public_key = None; loop { - let results = recv + let results = signer + .res_recv .recv_timeout(timeout) .expect("failed to recv dkg results"); for result in results { @@ -739,12 +725,10 @@ impl SignerTest { /// # Panics /// Panics if `signer_idx` is out of bounds fn stop_signer(&mut self, signer_idx: usize) -> StacksPrivateKey { - let running_signer = self.running_signers.remove(signer_idx); - self.signer_cmd_senders.remove(signer_idx); - self.result_receivers.remove(signer_idx); + let spawned_signer = self.spawned_signers.remove(signer_idx); let signer_key = self.signer_stacks_private_keys.remove(signer_idx); - running_signer.stop(); + spawned_signer.stop(); signer_key } @@ -765,15 +749,10 @@ impl SignerTest { .pop() .unwrap(); - let (cmd_send, cmd_recv) = channel(); - let (res_send, res_recv) = channel(); - info!("Restarting signer"); - let signer = spawn_signer(&signer_config, cmd_recv, res_send); - - self.result_receivers.insert(signer_idx, res_recv); - self.signer_cmd_senders.insert(signer_idx, cmd_send); - self.running_signers.insert(signer_idx, signer); + let config = SignerConfig::load_from_str(&signer_config).unwrap(); + let signer = SpawnedSigner::from(config); + self.spawned_signers.insert(signer_idx, signer); } fn shutdown(self) { @@ -787,37 +766,13 @@ impl SignerTest { .run_loop_stopper .store(false, Ordering::SeqCst); // Stop the signers before the node to prevent hanging - for signer in self.running_signers { + for signer in self.spawned_signers { assert!(signer.stop().is_none()); } self.running_nodes.run_loop_thread.join().unwrap(); } } -fn spawn_signer( - data: &str, - receiver: Receiver, - sender: Sender>, -) -> RunningSigner> { - let config = SignerConfig::load_from_str(data).unwrap(); - let ev = SignerEventReceiver::new(config.network.is_mainnet()); - let endpoint = config.endpoint; - #[cfg(feature = "monitoring_prom")] - { - stacks_signer::monitoring::start_serving_monitoring_metrics(config.clone()).ok(); - } - let runloop: stacks_signer::runloop::RunLoop = - stacks_signer::runloop::RunLoop::new(config); - let mut signer: Signer< - RunLoopCommand, - Vec, - stacks_signer::runloop::RunLoop, - SignerEventReceiver, - > = Signer::new(runloop, ev, receiver, sender); - info!("Spawning signer on endpoint {}", endpoint); - signer.spawn(endpoint).unwrap() -} - fn setup_stx_btc_node( mut naka_conf: NeonConfig, signer_stacks_private_keys: &[StacksPrivateKey], @@ -984,8 +939,9 @@ fn stackerdb_dkg() { // Determine the coordinator of the current node height info!("signer_runloop: spawn send commands to do dkg"); let dkg_now = Instant::now(); - for sender in signer_test.signer_cmd_senders.iter() { - sender + for signer in signer_test.spawned_signers.iter() { + signer + .cmd_send .send(RunLoopCommand { reward_cycle, command: SignerCommand::Dkg, @@ -1075,12 +1031,12 @@ fn stackerdb_sign_request_rejected() { info!("------------------------- Test Sign -------------------------"); let reward_cycle = signer_test.get_current_reward_cycle(); - let block_proposal_1 = BlockProposalSigners { + let block_proposal_1 = BlockProposal { block: block1.clone(), burn_height: 0, reward_cycle, }; - let block_proposal_2 = BlockProposalSigners { + let block_proposal_2 = BlockProposal { block: block2.clone(), burn_height: 0, reward_cycle, @@ -1104,11 +1060,13 @@ fn stackerdb_sign_request_rejected() { merkle_root: None, }, }; - for sender in signer_test.signer_cmd_senders.iter() { - sender + for signer in signer_test.spawned_signers.iter() { + signer + .cmd_send .send(sign_command.clone()) .expect("failed to send sign command"); - sender + signer + .cmd_send .send(sign_taproot_command.clone()) .expect("failed to send sign taproot command"); }