Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vote for cli command #4934

Merged
merged 5 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions stacks-signer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ libstackerdb = { path = "../libstackerdb" }
prometheus = { version = "0.9", optional = true }
rand_core = "0.6"
reqwest = { version = "0.11.22", default-features = false, features = ["blocking", "json", "rustls-tls"] }
sha2 = "0.10"
jferrant marked this conversation as resolved.
Show resolved Hide resolved
serde = "1"
serde_derive = "1"
serde_stacker = "0.1"
Expand Down
107 changes: 107 additions & 0 deletions stacks-signer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ use std::path::PathBuf;
use blockstack_lib::chainstate::stacks::address::PoxAddress;
use blockstack_lib::util_lib::signed_structured_data::pox4::Pox4SignatureTopic;
use clap::{ArgAction, Parser, ValueEnum};
use clarity::types::chainstate::StacksPublicKey;
use clarity::types::{PrivateKey, PublicKey};
use clarity::util::hash::Sha512Trunc256Sum;
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::types::QualifiedContractIdentifier;
use sha2::{Digest, Sha512_256};
use stacks_common::address::{
b58, AddressHashMode, C32_ADDRESS_VERSION_MAINNET_MULTISIG,
C32_ADDRESS_VERSION_MAINNET_SINGLESIG, C32_ADDRESS_VERSION_TESTNET_MULTISIG,
Expand Down Expand Up @@ -55,6 +60,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 +132,89 @@ pub struct RunSignerArgs {
pub config: PathBuf,
}

#[derive(Parser, Debug, Clone, Copy)]
/// Arguments for the Vote command
pub struct GenerateVoteArgs {
/// The Stacks private key to use in hexademical format
#[arg(short, long, value_parser = parse_private_key)]
pub private_key: StacksPrivateKey,
jferrant marked this conversation as resolved.
Show resolved Hide resolved
/// 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) -> Sha512Trunc256Sum {
let mut hasher = Sha512_256::new();
hasher.update(self.sip.to_be_bytes());
hasher.update((self.vote as u8).to_be_bytes());
Sha512Trunc256Sum::from_hasher(hasher)
}

/// 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())
}
jferrant marked this conversation as resolved.
Show resolved Hide resolved

/// 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)
}
}

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// A given vote for a SIP
pub enum Vote {
/// Vote yes
Yes,
/// Vote no
No,
}
jferrant marked this conversation as resolved.
Show resolved Hide resolved

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)),
}
}
}

#[derive(Clone, Debug, PartialEq)]
/// Wrapper around `Pox4SignatureTopic` to implement `ValueEnum`
pub struct StackingSignatureMethod(Pox4SignatureTopic);
Expand Down Expand Up @@ -233,6 +325,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_from(vote)
}

/// 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
113 changes: 110 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,29 @@ fn handle_check_config(args: RunSignerArgs) {
println!("Config: {}", config);
}

fn handle_generate_vote(args: GenerateVoteArgs, do_print: bool) -> MessageSignature {
let message_signature = args.vote_info.sign(&args.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 +217,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 +233,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::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 +369,80 @@ pub mod tests {
assert!(verify_result.is_ok());
assert!(verify_result.unwrap());
}

#[test]
fn test_vote() {
let mut rand = rand::thread_rng();
let private_key = Secp256k1PrivateKey::new();
let public_key = StacksPublicKey::from_private(&private_key);
let vote_info = VoteInfo {
vote: Vote::No,
sip: rand.next_u32(),
};
let args = GenerateVoteArgs {
private_key,
vote_info,
};
let message_signature = handle_generate_vote(args, false);
assert!(
args.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");
}
}