Skip to content

Commit

Permalink
Merge pull request #4934 from stacks-network/feature/vote-for-sip-cli
Browse files Browse the repository at this point in the history
Add vote for cli command
  • Loading branch information
wileyj committed Jul 3, 2024
2 parents b6b096e + bb21209 commit 1c8cd23
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 7 deletions.
125 changes: 124 additions & 1 deletion stacks-signer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,24 @@ use std::path::PathBuf;

use blockstack_lib::chainstate::stacks::address::PoxAddress;
use blockstack_lib::util_lib::signed_structured_data::pox4::Pox4SignatureTopic;
use blockstack_lib::util_lib::signed_structured_data::{
make_structured_data_domain, structured_data_message_hash,
};
use clap::{ArgAction, Parser, ValueEnum};
use clarity::vm::types::QualifiedContractIdentifier;
use clarity::consts::CHAIN_ID_MAINNET;
use clarity::types::chainstate::StacksPublicKey;
use clarity::types::{PrivateKey, PublicKey};
use clarity::util::hash::Sha256Sum;
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::types::{QualifiedContractIdentifier, TupleData};
use clarity::vm::Value;
use serde::{Deserialize, Serialize};
use stacks_common::address::{
b58, AddressHashMode, C32_ADDRESS_VERSION_MAINNET_MULTISIG,
C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_MULTISIG,
C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
};
use stacks_common::define_u8_enum;
use stacks_common::types::chainstate::StacksPrivateKey;

extern crate alloc;
Expand Down Expand Up @@ -55,6 +66,10 @@ pub enum Command {
GenerateStackingSignature(GenerateStackingSignatureArgs),
/// Check a configuration file and output config information
CheckConfig(RunSignerArgs),
/// Vote for a specified SIP with a yes or no vote
GenerateVote(GenerateVoteArgs),
/// Verify the vote for a specified SIP against a public key and vote info
VerifyVote(VerifyVoteArgs),
}

/// Basic arguments for all cyrptographic and stacker-db functionality
Expand Down Expand Up @@ -123,6 +138,99 @@ pub struct RunSignerArgs {
pub config: PathBuf,
}

#[derive(Parser, Debug, Clone)]
/// Arguments for the Vote command
pub struct GenerateVoteArgs {
/// Path to signer config file
#[arg(long, short, value_name = "FILE")]
pub config: PathBuf,
/// The vote info being cast
#[clap(flatten)]
pub vote_info: VoteInfo,
}

#[derive(Parser, Debug, Clone, Copy)]
/// Arguments for the VerifyVote command
pub struct VerifyVoteArgs {
/// The Stacks public key to verify against
#[arg(short, long, value_parser = parse_public_key)]
pub public_key: StacksPublicKey,
/// The message signature in hexadecimal format
#[arg(short, long, value_parser = parse_message_signature)]
pub signature: MessageSignature,
/// The vote info being verified
#[clap(flatten)]
pub vote_info: VoteInfo,
}

#[derive(Parser, Debug, Clone, Copy)]
/// Information about a SIP vote
pub struct VoteInfo {
/// The SIP number to vote on
#[arg(long)]
pub sip: u32,
/// The vote to cast
#[arg(long, value_parser = parse_vote)]
pub vote: Vote,
}

impl VoteInfo {
/// Get the digest to sign that authenticates this vote data
fn digest(&self) -> Sha256Sum {
let vote_message = TupleData::from_data(vec![
("sip".into(), Value::UInt(self.sip.into())),
("vote".into(), Value::UInt(self.vote.to_u8().into())),
])
.unwrap();
let data_domain =
make_structured_data_domain("signer-sip-voting", "1.0.0", CHAIN_ID_MAINNET);
structured_data_message_hash(vote_message.into(), data_domain)
}

/// Sign the vote data and return the signature
pub fn sign(&self, private_key: &StacksPrivateKey) -> Result<MessageSignature, &'static str> {
let digest = self.digest();
private_key.sign(digest.as_bytes())
}

/// Verify the vote data against the provided public key and signature
pub fn verify(
&self,
public_key: &StacksPublicKey,
signature: &MessageSignature,
) -> Result<bool, &'static str> {
let digest = self.digest();
public_key.verify(digest.as_bytes(), signature)
}
}

define_u8_enum!(
/// A given vote for a SIP
Vote {
/// Vote yes
Yes = 0,
/// Vote no
No = 1
});

impl TryFrom<&str> for Vote {
type Error = String;
fn try_from(input: &str) -> Result<Vote, Self::Error> {
match input.to_lowercase().as_str() {
"yes" => Ok(Vote::Yes),
"no" => Ok(Vote::No),
_ => Err(format!("Invalid vote: {}. Must be `yes` or `no`.", input)),
}
}
}

impl TryFrom<u8> for Vote {
type Error = String;
fn try_from(input: u8) -> Result<Vote, Self::Error> {
Vote::from_u8(input).ok_or_else(|| format!("Invalid vote: {}. Must be 0 or 1.", input))
}
}

#[derive(Clone, Debug, PartialEq)]
/// Wrapper around `Pox4SignatureTopic` to implement `ValueEnum`
pub struct StackingSignatureMethod(Pox4SignatureTopic);
Expand Down Expand Up @@ -233,6 +341,21 @@ fn parse_private_key(private_key: &str) -> Result<StacksPrivateKey, String> {
StacksPrivateKey::from_hex(private_key).map_err(|e| format!("Invalid private key: {}", e))
}

/// Parse the hexadecimal Stacks public key
fn parse_public_key(public_key: &str) -> Result<StacksPublicKey, String> {
StacksPublicKey::from_hex(public_key).map_err(|e| format!("Invalid public key: {}", e))
}

/// Parse the vote
fn parse_vote(vote: &str) -> Result<Vote, String> {
vote.try_into()
}

/// Parse the hexadecimal encoded message signature
fn parse_message_signature(signature: &str) -> Result<MessageSignature, String> {
MessageSignature::from_hex(signature).map_err(|e| format!("Invalid message signature: {}", e))
}

/// Parse the input data
fn parse_data(data: &str) -> Result<Vec<u8>, String> {
let encoded_data = if data == "-" {
Expand Down
2 changes: 0 additions & 2 deletions stacks-signer/src/client/stackerdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,6 @@ mod tests {
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
use clarity::util::hash::{MerkleTree, Sha512Trunc256Sum};
use libsigner::v0::messages::{BlockRejection, BlockResponse, RejectCode, SignerMessage};
use libsigner::BlockProposal;
use rand::{thread_rng, RngCore};

use super::*;
use crate::client::tests::{generate_signer_config, mock_server_from_config, write_response};
Expand Down
114 changes: 111 additions & 3 deletions stacks-signer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ use stacks_common::debug;
use stacks_common::util::hash::to_hex;
use stacks_common::util::secp256k1::MessageSignature;
use stacks_signer::cli::{
Cli, Command, GenerateStackingSignatureArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs,
RunSignerArgs, StackerDBArgs,
Cli, Command, GenerateStackingSignatureArgs, GenerateVoteArgs, GetChunkArgs,
GetLatestChunkArgs, PutChunkArgs, RunSignerArgs, StackerDBArgs, VerifyVoteArgs,
};
use stacks_signer::config::GlobalConfig;
use stacks_signer::v0::SpawnedSigner;
Expand Down Expand Up @@ -164,6 +164,30 @@ fn handle_check_config(args: RunSignerArgs) {
println!("Config: {}", config);
}

fn handle_generate_vote(args: GenerateVoteArgs, do_print: bool) -> MessageSignature {
let config = GlobalConfig::try_from(&args.config).unwrap();
let message_signature = args.vote_info.sign(&config.stacks_private_key).unwrap();
if do_print {
println!("{}", to_hex(message_signature.as_bytes()));
}
message_signature
}

fn handle_verify_vote(args: VerifyVoteArgs, do_print: bool) -> bool {
let valid_vote = args
.vote_info
.verify(&args.public_key, &args.signature)
.unwrap();
if do_print {
if valid_vote {
println!("Valid vote");
} else {
println!("Invalid vote");
}
}
valid_vote
}

fn main() {
let cli = Cli::parse();

Expand Down Expand Up @@ -194,6 +218,12 @@ fn main() {
Command::CheckConfig(args) => {
handle_check_config(args);
}
Command::GenerateVote(args) => {
handle_generate_vote(args, true);
}
Command::VerifyVote(args) => {
handle_verify_vote(args, true);
}
}
}

Expand All @@ -204,11 +234,13 @@ pub mod tests {
use blockstack_lib::util_lib::signed_structured_data::pox4::{
make_pox_4_signer_key_message_hash, Pox4SignatureTopic,
};
use clarity::util::secp256k1::Secp256k1PrivateKey;
use clarity::vm::{execute_v2, Value};
use rand::{Rng, RngCore};
use stacks_common::consts::CHAIN_ID_TESTNET;
use stacks_common::types::PublicKey;
use stacks_common::util::secp256k1::Secp256k1PublicKey;
use stacks_signer::cli::parse_pox_addr;
use stacks_signer::cli::{parse_pox_addr, VerifyVoteArgs, Vote, VoteInfo};

use super::{handle_generate_stacking_signature, *};
use crate::{GenerateStackingSignatureArgs, GlobalConfig};
Expand Down Expand Up @@ -338,4 +370,80 @@ pub mod tests {
assert!(verify_result.is_ok());
assert!(verify_result.unwrap());
}

#[test]
fn test_vote() {
let mut rand = rand::thread_rng();
let vote_info = VoteInfo {
vote: rand.gen_range(0..2).try_into().unwrap(),
sip: rand.next_u32(),
};
let config_file = "./src/tests/conf/signer-0.toml";
let config = GlobalConfig::load_from_file(config_file).unwrap();
let private_key = config.stacks_private_key;
let public_key = StacksPublicKey::from_private(&private_key);
let args = GenerateVoteArgs {
config: config_file.into(),
vote_info,
};
let message_signature = handle_generate_vote(args, false);
assert!(
vote_info.verify(&public_key, &message_signature).unwrap(),
"Vote should be valid"
);
}

#[test]
fn test_verify_vote() {
let mut rand = rand::thread_rng();
let private_key = Secp256k1PrivateKey::new();
let public_key = StacksPublicKey::from_private(&private_key);

let invalid_private_key = Secp256k1PrivateKey::new();
let invalid_public_key = StacksPublicKey::from_private(&invalid_private_key);

let sip = rand.next_u32();
let vote_info = VoteInfo {
vote: Vote::No,
sip,
};

let args = VerifyVoteArgs {
public_key,
signature: vote_info.sign(&private_key).unwrap(),
vote_info,
};
let valid = handle_verify_vote(args, false);
assert!(valid, "Vote should be valid");

let args = VerifyVoteArgs {
public_key: invalid_public_key,
signature: vote_info.sign(&private_key).unwrap(), // Invalid corresponding public key
vote_info,
};
let valid = handle_verify_vote(args, false);
assert!(!valid, "Vote should be invalid");

let args = VerifyVoteArgs {
public_key,
signature: vote_info.sign(&private_key).unwrap(),
vote_info: VoteInfo {
vote: Vote::Yes, // Invalid vote
sip,
},
};
let valid = handle_verify_vote(args, false);
assert!(!valid, "Vote should be invalid");

let args = VerifyVoteArgs {
public_key,
signature: vote_info.sign(&private_key).unwrap(),
vote_info: VoteInfo {
vote: Vote::No,
sip: sip.wrapping_add(1), // Invalid sip number
},
};
let valid = handle_verify_vote(args, false);
assert!(!valid, "Vote should be invalid");
}
}
2 changes: 1 addition & 1 deletion stacks-signer/src/v0/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl SignerTrait<SignerMessage> for Signer {
);
}
SignerMessage::BlockPushed(b) => {
let block_push_result = stacks_client.post_block(&b);
let block_push_result = stacks_client.post_block(b);
info!(
"{self}: Got block pushed message";
"block_id" => %b.block_id(),
Expand Down

0 comments on commit 1c8cd23

Please sign in to comment.