Move filtering of messages out ot stacks-signer into libsigner

Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>
This commit is contained in:
Jacinta Ferrant
2024-01-23 13:04:17 -08:00
parent e3db2f9c1e
commit a705604890
11 changed files with 347 additions and 335 deletions

1
Cargo.lock generated
View File

@@ -1971,6 +1971,7 @@ dependencies = [
name = "libsigner"
version = "0.0.1"
dependencies = [
"bincode",
"clarity",
"libc",
"libstackerdb",

View File

@@ -16,6 +16,7 @@ name = "libsigner"
path = "./src/libsigner.rs"
[dependencies]
bincode = "1.3.3"
clarity = { path = "../clarity" }
libc = "0.2"
libstackerdb = { path = "../libstackerdb" }

View File

@@ -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<BlockValidateReject> 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<Packet> for SignerMessage {
fn from(packet: Packet) -> Self {
Self::Packet(packet)
}
}
impl From<BlockResponse> for SignerMessage {
fn from(block_response: BlockResponse) -> Self {
Self::BlockResponse(block_response)
}
}
impl From<BlockRejection> for SignerMessage {
fn from(block_rejection: BlockRejection) -> Self {
Self::BlockResponse(BlockResponse::Rejected(block_rejection))
}
}
impl From<BlockValidateReject> 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<NakamotoBlock>),
/// The signer messages for other signers and miners to observe
SignerMessages(Vec<SignerMessage>),
/// 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<Sender<SignerEvent>>,
/// inter-thread stop variable -- if set to true, then the `main_loop` will exit
stop_signal: Arc<AtomicBool>,
/// 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<QualifiedContractIdentifier>) -> SignerEventReceiver {
pub fn new(
contract_ids: Vec<QualifiedContractIdentifier>,
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<F, R>(&mut self, todo: F) -> Result<R, EventError>
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<SignerEvent, EventError> {
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<SocketAddr>,
mut request: HttpRequest,
is_mainnet: bool,
) -> Result<SignerEvent, EventError> {
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<NakamotoBlock> = event
.modified_slots
.iter()
.filter_map(|chunk| read_next::<NakamotoBlock, _>(&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<SignerMessage> = event
.modified_slots
.iter()
.filter_map(|chunk| bincode::deserialize::<SignerMessage>(&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<SignerEvent, EventError> {
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))
}

View File

@@ -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};

View File

@@ -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<Vec<SignerEvent>, 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<SignerEvent> = 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();
}

View File

@@ -13,161 +13,18 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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<BlockValidateReject> 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<Packet> for SignerMessage {
fn from(packet: Packet) -> Self {
Self::Packet(packet)
}
}
impl From<BlockResponse> for SignerMessage {
fn from(block_response: BlockResponse) -> Self {
Self::BlockResponse(block_response)
}
}
impl From<BlockRejection> for SignerMessage {
fn from(block_rejection: BlockRejection) -> Self {
Self::BlockResponse(BlockResponse::Rejected(block_rejection))
}
}
impl From<BlockValidateReject> 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

View File

@@ -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

View File

@@ -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<FireCoordinator<v2::Aggregator>> = RunLoop::from(&config);
let mut signer: Signer<
RunLoopCommand,

View File

@@ -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<C: Coordinator> RunLoop<C> {
}
}
/// 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<Vec<OperationResult>>,
messages: Vec<SignerMessage>,
) {
let (_coordinator_id, coordinator_public_key) =
calculate_coordinator(&self.signing_round.public_keys, &self.stacks_client);
let inbound_packets: Vec<Packet> = stackerdb_chunk_event
.modified_slots
.iter()
.filter_map(|chunk| self.verify_chunk(chunk, &coordinator_public_key))
let packets: Vec<Packet> = 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::<NakamotoBlock, _>(&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<NakamotoBlock>) {
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<C: Coordinator> RunLoop<C> {
/// 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<Packet> {
// We only care about verified wsts packets. Ignore anything else
let signer_message = bincode::deserialize::<SignerMessage>(&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<C: Coordinator> SignerRunLoop<Vec<OperationResult>, 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.

View File

@@ -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;

View File

@@ -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<RunLoopCommand>,
// The channels for sending commands to the signers
pub signer_cmd_senders: HashMap<u32, Sender<RunLoopCommand>>,
pub _signer_cmd_senders: HashMap<u32, Sender<RunLoopCommand>>,
// The channels for receiving results from both the coordinator and the signers
pub result_receivers: Vec<Receiver<Vec<OperationResult>>>,
// 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<Vec<OperationResult>>,
) -> RunningSigner<SignerEventReceiver, Vec<OperationResult>> {
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<FireCoordinator<v2::Aggregator>> =
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;
}