Skip to content

Commit

Permalink
Merge pull request #5020 from stacks-network/feat/mock-signing-in-2.5
Browse files Browse the repository at this point in the history
Feat/mock signing in 2.5
  • Loading branch information
jferrant authored Jul 31, 2024
2 parents 228597e + c394a08 commit 608ad7a
Show file tree
Hide file tree
Showing 7 changed files with 725 additions and 17 deletions.
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
- tests::signer::v0::forked_tenure_invalid
- tests::signer::v0::empty_sortition
- tests::signer::v0::bitcoind_forking_test
- tests::signer::v0::mock_sign_epoch_25
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
- tests::nakamoto_integrations::check_block_heights
- tests::nakamoto_integrations::clarity_burn_state
Expand Down
275 changes: 272 additions & 3 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,27 @@ 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::getinfo::RPCPeerInfoData;
use blockstack_lib::net::api::postblock_proposal::{
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
};
use blockstack_lib::util_lib::boot::boot_code_id;
use blockstack_lib::util_lib::signed_structured_data::{
make_structured_data_domain, structured_data_message_hash,
};
use clarity::types::chainstate::{
BlockHeaderHash, ConsensusHash, StacksPrivateKey, StacksPublicKey,
};
use clarity::types::PrivateKey;
use clarity::util::hash::Sha256Sum;
use clarity::util::retry::BoundReader;
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::types::serialization::SerializationError;
use clarity::vm::types::QualifiedContractIdentifier;
use clarity::vm::types::{QualifiedContractIdentifier, TupleData};
use clarity::vm::Value;
use hashbrown::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512_256};
use stacks_common::codec::{
read_next, read_next_at_most, read_next_exact, write_next, Error as CodecError,
StacksMessageCodec,
Expand All @@ -55,6 +66,7 @@ use tiny_http::{
};

use crate::http::{decode_http_body, decode_http_request};
use crate::stacks_common::types::PublicKey;
use crate::{
BlockProposal, EventError, MessageSlotID as MessageSlotIDTrait,
SignerMessage as SignerMessageTrait,
Expand All @@ -65,7 +77,9 @@ define_u8_enum!(
/// the contract index in the signers contracts (i.e., X in signers-0-X)
MessageSlotID {
/// Block Response message from signers
BlockResponse = 1
BlockResponse = 1,
/// Mock Signature message from Epoch 2.5 signers
MockSignature = 2
});

define_u8_enum!(
Expand Down Expand Up @@ -100,7 +114,9 @@ SignerMessageTypePrefix {
/// Block Response message from signers
BlockResponse = 1,
/// Block Pushed message from miners
BlockPushed = 2
BlockPushed = 2,
/// Mock Signature message from Epoch 2.5 signers
MockSignature = 3
});

#[cfg_attr(test, mutants::skip)]
Expand Down Expand Up @@ -143,6 +159,7 @@ impl From<&SignerMessage> for SignerMessageTypePrefix {
SignerMessage::BlockProposal(_) => SignerMessageTypePrefix::BlockProposal,
SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse,
SignerMessage::BlockPushed(_) => SignerMessageTypePrefix::BlockPushed,
SignerMessage::MockSignature(_) => SignerMessageTypePrefix::MockSignature,
}
}
}
Expand All @@ -156,6 +173,8 @@ pub enum SignerMessage {
BlockResponse(BlockResponse),
/// A block pushed from miners to the signers set
BlockPushed(NakamotoBlock),
/// A mock signature from the epoch 2.5 signers
MockSignature(MockSignature),
}

impl SignerMessage {
Expand All @@ -167,6 +186,7 @@ impl SignerMessage {
match self {
Self::BlockProposal(_) | Self::BlockPushed(_) => None,
Self::BlockResponse(_) => Some(MessageSlotID::BlockResponse),
Self::MockSignature(_) => Some(MessageSlotID::MockSignature),
}
}
}
Expand All @@ -180,6 +200,7 @@ impl StacksMessageCodec for SignerMessage {
SignerMessage::BlockProposal(block_proposal) => block_proposal.consensus_serialize(fd),
SignerMessage::BlockResponse(block_response) => block_response.consensus_serialize(fd),
SignerMessage::BlockPushed(block) => block.consensus_serialize(fd),
SignerMessage::MockSignature(signature) => signature.consensus_serialize(fd),
}?;
Ok(())
}
Expand All @@ -201,6 +222,10 @@ impl StacksMessageCodec for SignerMessage {
let block = StacksMessageCodec::consensus_deserialize(fd)?;
SignerMessage::BlockPushed(block)
}
SignerMessageTypePrefix::MockSignature => {
let signature = StacksMessageCodec::consensus_deserialize(fd)?;
SignerMessage::MockSignature(signature)
}
};
Ok(message)
}
Expand All @@ -214,6 +239,178 @@ pub trait StacksMessageCodecExtensions: Sized {
fn inner_consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError>;
}

/// A snapshot of the signer view of the stacks node to be used for mock signing.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MockSignData {
/// The stacks tip consensus hash at the time of the mock signature
pub stacks_tip_consensus_hash: ConsensusHash,
/// The stacks tip header hash at the time of the mock signature
pub stacks_tip: BlockHeaderHash,
/// The server version
pub server_version: String,
/// The burn block height that triggered the mock signature
pub burn_block_height: u64,
/// The burn block height of the peer view at the time of the mock signature. Note
/// that this may be different from the burn_block_height if the peer view is stale.
pub peer_burn_block_height: u64,
/// The POX consensus hash at the time of the mock signature
pub pox_consensus: ConsensusHash,
/// The chain id for the mock signature
pub chain_id: u32,
}

impl MockSignData {
fn new(peer_view: RPCPeerInfoData, burn_block_height: u64, chain_id: u32) -> Self {
Self {
stacks_tip_consensus_hash: peer_view.stacks_tip_consensus_hash,
stacks_tip: peer_view.stacks_tip,
server_version: peer_view.server_version,
burn_block_height,
peer_burn_block_height: peer_view.burn_block_height,
pox_consensus: peer_view.pox_consensus,
chain_id,
}
}
}

impl StacksMessageCodec for MockSignData {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
write_next(fd, self.stacks_tip_consensus_hash.as_bytes())?;
write_next(fd, &self.stacks_tip)?;
write_next(fd, &(self.server_version.as_bytes().len() as u8))?;
fd.write_all(self.server_version.as_bytes())
.map_err(CodecError::WriteError)?;
write_next(fd, &self.burn_block_height)?;
write_next(fd, &self.peer_burn_block_height)?;
write_next(fd, &self.pox_consensus)?;
write_next(fd, &self.chain_id)?;
Ok(())
}

fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
let stacks_tip_consensus_hash = read_next::<ConsensusHash, _>(fd)?;
let stacks_tip = read_next::<BlockHeaderHash, _>(fd)?;
let len_byte: u8 = read_next(fd)?;
let mut bytes = vec![0u8; len_byte as usize];
fd.read_exact(&mut bytes).map_err(CodecError::ReadError)?;
// must encode a valid string
let server_version = String::from_utf8(bytes).map_err(|_e| {
CodecError::DeserializeError(
"Failed to parse server version name: could not contruct from utf8".to_string(),
)
})?;
let burn_block_height = read_next::<u64, _>(fd)?;
let peer_burn_block_height = read_next::<u64, _>(fd)?;
let pox_consensus = read_next::<ConsensusHash, _>(fd)?;
let chain_id = read_next::<u32, _>(fd)?;
Ok(Self {
stacks_tip_consensus_hash,
stacks_tip,
server_version,
burn_block_height,
peer_burn_block_height,
pox_consensus,
chain_id,
})
}
}

/// A mock signature for the stacks node to be used for mock signing.
/// This is only used by Epoch 2.5 signers to simulate the signing of a block for every sortition.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MockSignature {
/// The signature of the mock signature
signature: MessageSignature,
/// The data that was signed across
pub sign_data: MockSignData,
}

impl MockSignature {
/// Create a new mock sign data struct from the provided peer info, burn block height, chain id, and private key.
pub fn new(
peer_view: RPCPeerInfoData,
burn_block_height: u64,
chain_id: u32,
stacks_private_key: &StacksPrivateKey,
) -> Self {
let mut sig = Self {
signature: MessageSignature::empty(),
sign_data: MockSignData::new(peer_view, burn_block_height, chain_id),
};
sig.sign(stacks_private_key)
.expect("Failed to sign MockSignature");
sig
}

/// The signature hash for the mock signature
pub fn signature_hash(&self) -> Sha256Sum {
let domain_tuple =
make_structured_data_domain("mock-signer", "1.0.0", self.sign_data.chain_id);
let data_tuple = Value::Tuple(
TupleData::from_data(vec![
(
"stacks-tip-consensus-hash".into(),
Value::buff_from(self.sign_data.stacks_tip_consensus_hash.as_bytes().into())
.unwrap(),
),
(
"stacks-tip".into(),
Value::buff_from(self.sign_data.stacks_tip.as_bytes().into()).unwrap(),
),
(
"server-version".into(),
Value::string_ascii_from_bytes(self.sign_data.server_version.clone().into())
.unwrap(),
),
(
"burn-block-height".into(),
Value::UInt(self.sign_data.burn_block_height.into()),
),
(
"pox-consensus".into(),
Value::buff_from(self.sign_data.pox_consensus.as_bytes().into()).unwrap(),
),
])
.expect("Error creating signature hash"),
);
structured_data_message_hash(data_tuple, domain_tuple)
}

/// Sign the mock signature and set the internal signature field
fn sign(&mut self, private_key: &StacksPrivateKey) -> Result<(), String> {
let signature_hash = self.signature_hash();
self.signature = private_key.sign(signature_hash.as_bytes())?;
Ok(())
}
/// Verify the mock signature against the provided public key
pub fn verify(&self, public_key: &StacksPublicKey) -> Result<bool, String> {
if self.signature == MessageSignature::empty() {
return Ok(false);
}
let signature_hash = self.signature_hash();
public_key
.verify(&signature_hash.0, &self.signature)
.map_err(|e| e.to_string())
}
}

impl StacksMessageCodec for MockSignature {
fn consensus_serialize<W: Write>(&self, fd: &mut W) -> Result<(), CodecError> {
write_next(fd, &self.signature)?;
self.sign_data.consensus_serialize(fd)?;
Ok(())
}

fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
let signature = read_next::<MessageSignature, _>(fd)?;
let sign_data = read_next::<MockSignData, _>(fd)?;
Ok(Self {
signature,
sign_data,
})
}
}

define_u8_enum!(
/// Enum representing the reject code type prefix
RejectCodeTypePrefix {
Expand Down Expand Up @@ -507,7 +704,9 @@ mod test {
TransactionPostConditionMode, TransactionSmartContract, TransactionVersion,
};
use blockstack_lib::util_lib::strings::StacksString;
use clarity::consts::CHAIN_ID_MAINNET;
use clarity::types::chainstate::{ConsensusHash, StacksBlockId, TrieHash};
use clarity::types::PrivateKey;
use clarity::util::hash::MerkleTree;
use clarity::util::secp256k1::MessageSignature;
use rand::{thread_rng, Rng, RngCore};
Expand Down Expand Up @@ -622,4 +821,74 @@ mod test {
.expect("Failed to deserialize SignerMessage");
assert_eq!(signer_message, deserialized_signer_message);
}

fn random_mock_sign_data() -> MockSignData {
let stacks_tip_consensus_byte: u8 = thread_rng().gen();
let stacks_tip_byte: u8 = thread_rng().gen();
let pox_consensus_byte: u8 = thread_rng().gen();
let chain_byte: u8 = thread_rng().gen_range(0..=1);
let chain_id = if chain_byte == 1 {
CHAIN_ID_TESTNET
} else {
CHAIN_ID_MAINNET
};
MockSignData {
stacks_tip_consensus_hash: ConsensusHash([stacks_tip_consensus_byte; 20]),
stacks_tip: BlockHeaderHash([stacks_tip_byte; 32]),
server_version: "0.0.0".to_string(),
burn_block_height: thread_rng().next_u64(),
peer_burn_block_height: thread_rng().next_u64(),
pox_consensus: ConsensusHash([pox_consensus_byte; 20]),
chain_id,
}
}

#[test]
fn verify_sign_mock_signature() {
let private_key = StacksPrivateKey::new();
let public_key = StacksPublicKey::from_private(&private_key);

let bad_private_key = StacksPrivateKey::new();
let bad_public_key = StacksPublicKey::from_private(&bad_private_key);

let mut mock_signature = MockSignature {
signature: MessageSignature::empty(),
sign_data: random_mock_sign_data(),
};
assert!(!mock_signature
.verify(&public_key)
.expect("Failed to verify MockSignature"));

mock_signature
.sign(&private_key)
.expect("Failed to sign MockSignature");

assert!(mock_signature
.verify(&public_key)
.expect("Failed to verify MockSignature"));
assert!(!mock_signature
.verify(&bad_public_key)
.expect("Failed to verify MockSignature"));
}

#[test]
fn serde_mock_signature() {
let mock_signature = MockSignature {
signature: MessageSignature::empty(),
sign_data: random_mock_sign_data(),
};
let serialized_signature = mock_signature.serialize_to_vec();
let deserialized_signature = read_next::<MockSignature, _>(&mut &serialized_signature[..])
.expect("Failed to deserialize MockSignature");
assert_eq!(mock_signature, deserialized_signature);
}

#[test]
fn serde_sign_data() {
let sign_data = random_mock_sign_data();
let serialized_data = sign_data.serialize_to_vec();
let deserialized_data = read_next::<MockSignData, _>(&mut &serialized_data[..])
.expect("Failed to deserialize MockSignData");
assert_eq!(sign_data, deserialized_data);
}
}
Loading

0 comments on commit 608ad7a

Please sign in to comment.