Skip to content

Commit 2579dca

Browse files
authored
Merge branch 'develop' into chore/signer-logging
2 parents 0a0c5a4 + 608ad7a commit 2579dca

File tree

9 files changed

+782
-35
lines changed

9 files changed

+782
-35
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jobs:
9191
- tests::signer::v0::forked_tenure_invalid
9292
- tests::signer::v0::empty_sortition
9393
- tests::signer::v0::bitcoind_forking_test
94+
- tests::signer::v0::mock_sign_epoch_25
9495
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
9596
- tests::nakamoto_integrations::check_block_heights
9697
- tests::nakamoto_integrations::clarity_burn_state

libsigner/src/v0/messages.rs

Lines changed: 272 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,27 @@ use blockstack_lib::chainstate::nakamoto::signer_set::NakamotoSigners;
3434
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
3535
use blockstack_lib::chainstate::stacks::events::StackerDBChunksEvent;
3636
use blockstack_lib::chainstate::stacks::StacksTransaction;
37+
use blockstack_lib::net::api::getinfo::RPCPeerInfoData;
3738
use blockstack_lib::net::api::postblock_proposal::{
3839
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
3940
};
4041
use blockstack_lib::util_lib::boot::boot_code_id;
42+
use blockstack_lib::util_lib::signed_structured_data::{
43+
make_structured_data_domain, structured_data_message_hash,
44+
};
45+
use clarity::types::chainstate::{
46+
BlockHeaderHash, ConsensusHash, StacksPrivateKey, StacksPublicKey,
47+
};
48+
use clarity::types::PrivateKey;
49+
use clarity::util::hash::Sha256Sum;
4150
use clarity::util::retry::BoundReader;
4251
use clarity::util::secp256k1::MessageSignature;
4352
use clarity::vm::types::serialization::SerializationError;
44-
use clarity::vm::types::QualifiedContractIdentifier;
53+
use clarity::vm::types::{QualifiedContractIdentifier, TupleData};
54+
use clarity::vm::Value;
4555
use hashbrown::{HashMap, HashSet};
4656
use serde::{Deserialize, Serialize};
57+
use sha2::{Digest, Sha512_256};
4758
use stacks_common::codec::{
4859
read_next, read_next_at_most, read_next_exact, write_next, Error as CodecError,
4960
StacksMessageCodec,
@@ -55,6 +66,7 @@ use tiny_http::{
5566
};
5667

5768
use crate::http::{decode_http_body, decode_http_request};
69+
use crate::stacks_common::types::PublicKey;
5870
use crate::{
5971
BlockProposal, EventError, MessageSlotID as MessageSlotIDTrait,
6072
SignerMessage as SignerMessageTrait,
@@ -65,7 +77,9 @@ define_u8_enum!(
6577
/// the contract index in the signers contracts (i.e., X in signers-0-X)
6678
MessageSlotID {
6779
/// Block Response message from signers
68-
BlockResponse = 1
80+
BlockResponse = 1,
81+
/// Mock Signature message from Epoch 2.5 signers
82+
MockSignature = 2
6983
});
7084

7185
define_u8_enum!(
@@ -100,7 +114,9 @@ SignerMessageTypePrefix {
100114
/// Block Response message from signers
101115
BlockResponse = 1,
102116
/// Block Pushed message from miners
103-
BlockPushed = 2
117+
BlockPushed = 2,
118+
/// Mock Signature message from Epoch 2.5 signers
119+
MockSignature = 3
104120
});
105121

106122
#[cfg_attr(test, mutants::skip)]
@@ -143,6 +159,7 @@ impl From<&SignerMessage> for SignerMessageTypePrefix {
143159
SignerMessage::BlockProposal(_) => SignerMessageTypePrefix::BlockProposal,
144160
SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse,
145161
SignerMessage::BlockPushed(_) => SignerMessageTypePrefix::BlockPushed,
162+
SignerMessage::MockSignature(_) => SignerMessageTypePrefix::MockSignature,
146163
}
147164
}
148165
}
@@ -156,6 +173,8 @@ pub enum SignerMessage {
156173
BlockResponse(BlockResponse),
157174
/// A block pushed from miners to the signers set
158175
BlockPushed(NakamotoBlock),
176+
/// A mock signature from the epoch 2.5 signers
177+
MockSignature(MockSignature),
159178
}
160179

161180
impl SignerMessage {
@@ -167,6 +186,7 @@ impl SignerMessage {
167186
match self {
168187
Self::BlockProposal(_) | Self::BlockPushed(_) => None,
169188
Self::BlockResponse(_) => Some(MessageSlotID::BlockResponse),
189+
Self::MockSignature(_) => Some(MessageSlotID::MockSignature),
170190
}
171191
}
172192
}
@@ -180,6 +200,7 @@ impl StacksMessageCodec for SignerMessage {
180200
SignerMessage::BlockProposal(block_proposal) => block_proposal.consensus_serialize(fd),
181201
SignerMessage::BlockResponse(block_response) => block_response.consensus_serialize(fd),
182202
SignerMessage::BlockPushed(block) => block.consensus_serialize(fd),
203+
SignerMessage::MockSignature(signature) => signature.consensus_serialize(fd),
183204
}?;
184205
Ok(())
185206
}
@@ -201,6 +222,10 @@ impl StacksMessageCodec for SignerMessage {
201222
let block = StacksMessageCodec::consensus_deserialize(fd)?;
202223
SignerMessage::BlockPushed(block)
203224
}
225+
SignerMessageTypePrefix::MockSignature => {
226+
let signature = StacksMessageCodec::consensus_deserialize(fd)?;
227+
SignerMessage::MockSignature(signature)
228+
}
204229
};
205230
Ok(message)
206231
}
@@ -214,6 +239,178 @@ pub trait StacksMessageCodecExtensions: Sized {
214239
fn inner_consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError>;
215240
}
216241

242+
/// A snapshot of the signer view of the stacks node to be used for mock signing.
243+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244+
pub struct MockSignData {
245+
/// The stacks tip consensus hash at the time of the mock signature
246+
pub stacks_tip_consensus_hash: ConsensusHash,
247+
/// The stacks tip header hash at the time of the mock signature
248+
pub stacks_tip: BlockHeaderHash,
249+
/// The server version
250+
pub server_version: String,
251+
/// The burn block height that triggered the mock signature
252+
pub burn_block_height: u64,
253+
/// The burn block height of the peer view at the time of the mock signature. Note
254+
/// that this may be different from the burn_block_height if the peer view is stale.
255+
pub peer_burn_block_height: u64,
256+
/// The POX consensus hash at the time of the mock signature
257+
pub pox_consensus: ConsensusHash,
258+
/// The chain id for the mock signature
259+
pub chain_id: u32,
260+
}
261+
262+
impl MockSignData {
263+
fn new(peer_view: RPCPeerInfoData, burn_block_height: u64, chain_id: u32) -> Self {
264+
Self {
265+
stacks_tip_consensus_hash: peer_view.stacks_tip_consensus_hash,
266+
stacks_tip: peer_view.stacks_tip,
267+
server_version: peer_view.server_version,
268+
burn_block_height,
269+
peer_burn_block_height: peer_view.burn_block_height,
270+
pox_consensus: peer_view.pox_consensus,
271+
chain_id,
272+
}
273+
}
274+
}
275+
276+
impl StacksMessageCodec for MockSignData {
277+
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
278+
write_next(fd, self.stacks_tip_consensus_hash.as_bytes())?;
279+
write_next(fd, &self.stacks_tip)?;
280+
write_next(fd, &(self.server_version.as_bytes().len() as u8))?;
281+
fd.write_all(self.server_version.as_bytes())
282+
.map_err(CodecError::WriteError)?;
283+
write_next(fd, &self.burn_block_height)?;
284+
write_next(fd, &self.peer_burn_block_height)?;
285+
write_next(fd, &self.pox_consensus)?;
286+
write_next(fd, &self.chain_id)?;
287+
Ok(())
288+
}
289+
290+
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
291+
let stacks_tip_consensus_hash = read_next::<ConsensusHash, _>(fd)?;
292+
let stacks_tip = read_next::<BlockHeaderHash, _>(fd)?;
293+
let len_byte: u8 = read_next(fd)?;
294+
let mut bytes = vec![0u8; len_byte as usize];
295+
fd.read_exact(&mut bytes).map_err(CodecError::ReadError)?;
296+
// must encode a valid string
297+
let server_version = String::from_utf8(bytes).map_err(|_e| {
298+
CodecError::DeserializeError(
299+
"Failed to parse server version name: could not contruct from utf8".to_string(),
300+
)
301+
})?;
302+
let burn_block_height = read_next::<u64, _>(fd)?;
303+
let peer_burn_block_height = read_next::<u64, _>(fd)?;
304+
let pox_consensus = read_next::<ConsensusHash, _>(fd)?;
305+
let chain_id = read_next::<u32, _>(fd)?;
306+
Ok(Self {
307+
stacks_tip_consensus_hash,
308+
stacks_tip,
309+
server_version,
310+
burn_block_height,
311+
peer_burn_block_height,
312+
pox_consensus,
313+
chain_id,
314+
})
315+
}
316+
}
317+
318+
/// A mock signature for the stacks node to be used for mock signing.
319+
/// This is only used by Epoch 2.5 signers to simulate the signing of a block for every sortition.
320+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321+
pub struct MockSignature {
322+
/// The signature of the mock signature
323+
signature: MessageSignature,
324+
/// The data that was signed across
325+
pub sign_data: MockSignData,
326+
}
327+
328+
impl MockSignature {
329+
/// Create a new mock sign data struct from the provided peer info, burn block height, chain id, and private key.
330+
pub fn new(
331+
peer_view: RPCPeerInfoData,
332+
burn_block_height: u64,
333+
chain_id: u32,
334+
stacks_private_key: &StacksPrivateKey,
335+
) -> Self {
336+
let mut sig = Self {
337+
signature: MessageSignature::empty(),
338+
sign_data: MockSignData::new(peer_view, burn_block_height, chain_id),
339+
};
340+
sig.sign(stacks_private_key)
341+
.expect("Failed to sign MockSignature");
342+
sig
343+
}
344+
345+
/// The signature hash for the mock signature
346+
pub fn signature_hash(&self) -> Sha256Sum {
347+
let domain_tuple =
348+
make_structured_data_domain("mock-signer", "1.0.0", self.sign_data.chain_id);
349+
let data_tuple = Value::Tuple(
350+
TupleData::from_data(vec![
351+
(
352+
"stacks-tip-consensus-hash".into(),
353+
Value::buff_from(self.sign_data.stacks_tip_consensus_hash.as_bytes().into())
354+
.unwrap(),
355+
),
356+
(
357+
"stacks-tip".into(),
358+
Value::buff_from(self.sign_data.stacks_tip.as_bytes().into()).unwrap(),
359+
),
360+
(
361+
"server-version".into(),
362+
Value::string_ascii_from_bytes(self.sign_data.server_version.clone().into())
363+
.unwrap(),
364+
),
365+
(
366+
"burn-block-height".into(),
367+
Value::UInt(self.sign_data.burn_block_height.into()),
368+
),
369+
(
370+
"pox-consensus".into(),
371+
Value::buff_from(self.sign_data.pox_consensus.as_bytes().into()).unwrap(),
372+
),
373+
])
374+
.expect("Error creating signature hash"),
375+
);
376+
structured_data_message_hash(data_tuple, domain_tuple)
377+
}
378+
379+
/// Sign the mock signature and set the internal signature field
380+
fn sign(&mut self, private_key: &StacksPrivateKey) -> Result<(), String> {
381+
let signature_hash = self.signature_hash();
382+
self.signature = private_key.sign(signature_hash.as_bytes())?;
383+
Ok(())
384+
}
385+
/// Verify the mock signature against the provided public key
386+
pub fn verify(&self, public_key: &StacksPublicKey) -> Result<bool, String> {
387+
if self.signature == MessageSignature::empty() {
388+
return Ok(false);
389+
}
390+
let signature_hash = self.signature_hash();
391+
public_key
392+
.verify(&signature_hash.0, &self.signature)
393+
.map_err(|e| e.to_string())
394+
}
395+
}
396+
397+
impl StacksMessageCodec for MockSignature {
398+
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
399+
write_next(fd, &self.signature)?;
400+
self.sign_data.consensus_serialize(fd)?;
401+
Ok(())
402+
}
403+
404+
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
405+
let signature = read_next::<MessageSignature, _>(fd)?;
406+
let sign_data = read_next::<MockSignData, _>(fd)?;
407+
Ok(Self {
408+
signature,
409+
sign_data,
410+
})
411+
}
412+
}
413+
217414
define_u8_enum!(
218415
/// Enum representing the reject code type prefix
219416
RejectCodeTypePrefix {
@@ -507,7 +704,9 @@ mod test {
507704
TransactionPostConditionMode, TransactionSmartContract, TransactionVersion,
508705
};
509706
use blockstack_lib::util_lib::strings::StacksString;
707+
use clarity::consts::CHAIN_ID_MAINNET;
510708
use clarity::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash};
709+
use clarity::types::PrivateKey;
511710
use clarity::util::hash::MerkleTree;
512711
use clarity::util::secp256k1::MessageSignature;
513712
use rand::{thread_rng, Rng, RngCore};
@@ -622,4 +821,74 @@ mod test {
622821
.expect("Failed to deserialize SignerMessage");
623822
assert_eq!(signer_message, deserialized_signer_message);
624823
}
824+
825+
fn random_mock_sign_data() -> MockSignData {
826+
let stacks_tip_consensus_byte: u8 = thread_rng().gen();
827+
let stacks_tip_byte: u8 = thread_rng().gen();
828+
let pox_consensus_byte: u8 = thread_rng().gen();
829+
let chain_byte: u8 = thread_rng().gen_range(0..=1);
830+
let chain_id = if chain_byte == 1 {
831+
CHAIN_ID_TESTNET
832+
} else {
833+
CHAIN_ID_MAINNET
834+
};
835+
MockSignData {
836+
stacks_tip_consensus_hash: ConsensusHash([stacks_tip_consensus_byte; 20]),
837+
stacks_tip: BlockHeaderHash([stacks_tip_byte; 32]),
838+
server_version: "0.0.0".to_string(),
839+
burn_block_height: thread_rng().next_u64(),
840+
peer_burn_block_height: thread_rng().next_u64(),
841+
pox_consensus: ConsensusHash([pox_consensus_byte; 20]),
842+
chain_id,
843+
}
844+
}
845+
846+
#[test]
847+
fn verify_sign_mock_signature() {
848+
let private_key = StacksPrivateKey::new();
849+
let public_key = StacksPublicKey::from_private(&private_key);
850+
851+
let bad_private_key = StacksPrivateKey::new();
852+
let bad_public_key = StacksPublicKey::from_private(&bad_private_key);
853+
854+
let mut mock_signature = MockSignature {
855+
signature: MessageSignature::empty(),
856+
sign_data: random_mock_sign_data(),
857+
};
858+
assert!(!mock_signature
859+
.verify(&public_key)
860+
.expect("Failed to verify MockSignature"));
861+
862+
mock_signature
863+
.sign(&private_key)
864+
.expect("Failed to sign MockSignature");
865+
866+
assert!(mock_signature
867+
.verify(&public_key)
868+
.expect("Failed to verify MockSignature"));
869+
assert!(!mock_signature
870+
.verify(&bad_public_key)
871+
.expect("Failed to verify MockSignature"));
872+
}
873+
874+
#[test]
875+
fn serde_mock_signature() {
876+
let mock_signature = MockSignature {
877+
signature: MessageSignature::empty(),
878+
sign_data: random_mock_sign_data(),
879+
};
880+
let serialized_signature = mock_signature.serialize_to_vec();
881+
let deserialized_signature = read_next::<MockSignature, _>(&mut &serialized_signature[..])
882+
.expect("Failed to deserialize MockSignature");
883+
assert_eq!(mock_signature, deserialized_signature);
884+
}
885+
886+
#[test]
887+
fn serde_sign_data() {
888+
let sign_data = random_mock_sign_data();
889+
let serialized_data = sign_data.serialize_to_vec();
890+
let deserialized_data = read_next::<MockSignData, _>(&mut &serialized_data[..])
891+
.expect("Failed to deserialize MockSignData");
892+
assert_eq!(sign_data, deserialized_data);
893+
}
625894
}

0 commit comments

Comments
 (0)