Skip to content
This repository has been archived by the owner on Aug 22, 2024. It is now read-only.

Commit

Permalink
Merge pull request stacks-network#4319 from stacks-network/feat/signe…
Browse files Browse the repository at this point in the history
…r-cli-stacking-sig

feat: add signer CLI function to generate signature
  • Loading branch information
saralab authored Feb 9, 2024
2 parents be5ea05 + 38a7465 commit 9690ecd
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 10 deletions.
3 changes: 2 additions & 1 deletion stacks-signer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ slog = { version = "2.5.2", features = [ "max_level_trace" ] }
slog-json = { version = "2.3.0", optional = true }
slog-term = "2.6.0"
stacks-common = { path = "../stacks-common" }
stackslib = { path = "../stackslib"}
stackslib = { path = "../stackslib" }
thiserror = "1.0"
toml = "0.5.6"
tracing = "0.1.37"
Expand All @@ -45,6 +45,7 @@ rand = { workspace = true }

[dev-dependencies]
serial_test = "3.0.0"
clarity = { path = "../clarity", features = ["testing"] }

[dependencies.serde_json]
version = "1.0"
Expand Down
135 changes: 134 additions & 1 deletion stacks-signer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ use std::io::{self, Read};
use std::net::SocketAddr;
use std::path::PathBuf;

use clap::Parser;
use blockstack_lib::chainstate::stacks::address::PoxAddress;
use blockstack_lib::util_lib::signed_structured_data::pox4::Pox4SignatureTopic;
use clap::{Parser, ValueEnum};
use clarity::vm::types::QualifiedContractIdentifier;
use stacks_common::address::b58;
use stacks_common::types::chainstate::StacksPrivateKey;
Expand Down Expand Up @@ -56,6 +58,8 @@ pub enum Command {
Run(RunDkgArgs),
/// Generate necessary files for running a collection of signers
GenerateFiles(GenerateFilesArgs),
/// Generate a signature for Stacking transactions
GenerateStackingSignature(GenerateStackingSignatureArgs),
}

/// Basic arguments for all cyrptographic and stacker-db functionality
Expand Down Expand Up @@ -170,11 +174,83 @@ pub struct GenerateFilesArgs {
pub timeout: Option<u64>,
}

#[derive(Clone, Debug)]
/// Wrapper around `Pox4SignatureTopic` to implement `ValueEnum`
pub struct StackingSignatureMethod(Pox4SignatureTopic);

impl StackingSignatureMethod {
/// Get the inner `Pox4SignatureTopic`
pub fn topic(&self) -> &Pox4SignatureTopic {
&self.0
}
}

impl From<Pox4SignatureTopic> for StackingSignatureMethod {
fn from(topic: Pox4SignatureTopic) -> Self {
StackingSignatureMethod(topic)
}
}

impl ValueEnum for StackingSignatureMethod {
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(clap::builder::PossibleValue::new(self.0.get_name_str()))
}

fn value_variants<'a>() -> &'a [Self] {
&[
StackingSignatureMethod(Pox4SignatureTopic::StackStx),
StackingSignatureMethod(Pox4SignatureTopic::StackExtend),
StackingSignatureMethod(Pox4SignatureTopic::AggregationCommit),
]
}

fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
let topic = match input {
"stack-stx" => Pox4SignatureTopic::StackStx,
"stack-extend" => Pox4SignatureTopic::StackExtend,
"aggregation-commit" => Pox4SignatureTopic::AggregationCommit,
"agg-commit" => Pox4SignatureTopic::AggregationCommit,
_ => return Err(format!("Invalid topic: {}", input)),
};
Ok(topic.into())
}
}

#[derive(Parser, Debug, Clone)]
/// Arguments for the generate-stacking-signature command
pub struct GenerateStackingSignatureArgs {
/// BTC address used to receive rewards
#[arg(short, long, value_parser = parse_pox_addr)]
pub pox_address: PoxAddress,
/// The reward cycle to be used in the signature's message hash
#[arg(short, long)]
pub reward_cycle: u64,
/// Path to config file
#[arg(long, value_name = "FILE")]
pub config: PathBuf,
/// Topic for signature
#[arg(long)]
pub method: StackingSignatureMethod,
/// Number of cycles used as a lock period.
/// Use `1` for stack-aggregation-commit
#[arg(long)]
pub period: u64,
}

/// Parse the contract ID
fn parse_contract(contract: &str) -> Result<QualifiedContractIdentifier, String> {
QualifiedContractIdentifier::parse(contract).map_err(|e| format!("Invalid contract: {}", e))
}

/// Parse a BTC address argument and return a `PoxAddress`
pub fn parse_pox_addr(pox_address_literal: &str) -> Result<PoxAddress, String> {
if let Some(pox_address) = PoxAddress::from_b58(pox_address_literal) {
Ok(pox_address)
} else {
Err(format!("Invalid pox address: {}", pox_address_literal))
}
}

/// Parse the hexadecimal Stacks private key
fn parse_private_key(private_key: &str) -> Result<StacksPrivateKey, String> {
StacksPrivateKey::from_hex(private_key).map_err(|e| format!("Invalid private key: {}", e))
Expand Down Expand Up @@ -209,3 +285,60 @@ fn parse_network(network: &str) -> Result<Network, String> {
}
})
}

#[cfg(test)]
mod tests {
use blockstack_lib::chainstate::stacks::address::{PoxAddressType20, PoxAddressType32};

use super::*;

#[test]
fn test_parse_pox_addr() {
let tr = "bc1p8vg588hldsnv4a558apet4e9ff3pr4awhqj2hy8gy6x2yxzjpmqsvvpta4";
let pox_addr = parse_pox_addr(tr).expect("Failed to parse segwit address");
match pox_addr {
PoxAddress::Addr32(_, addr_type, _) => {
assert_eq!(addr_type, PoxAddressType32::P2TR);
}
_ => panic!("Invalid parsed address"),
}

let legacy = "1N8GMS991YDY1E696e9SB9EsYY5ckSU7hZ";
let pox_addr = parse_pox_addr(legacy).expect("Failed to parse legacy address");
match pox_addr {
PoxAddress::Standard(stacks_addr, hash_mode) => {
assert_eq!(stacks_addr.version, 22);
assert!(hash_mode.is_none());
}
_ => panic!("Invalid parsed address"),
}

let p2sh = "33JNgVMNMC9Xm6mJG9oTVf5zWbmt5xi1Mv";
let pox_addr = parse_pox_addr(p2sh).expect("Failed to parse legacy address");
match pox_addr {
PoxAddress::Standard(stacks_addr, hash_mode) => {
assert_eq!(stacks_addr.version, 20);
assert!(hash_mode.is_none());
}
_ => panic!("Invalid parsed address"),
}

let wsh = "bc1qvnpcphdctvmql5gdw6chtwvvsl6ra9gwa2nehc99np7f24juc4vqrx29cs";
let pox_addr = parse_pox_addr(wsh).expect("Failed to parse segwit address");
match pox_addr {
PoxAddress::Addr32(_, addr_type, _) => {
assert_eq!(addr_type, PoxAddressType32::P2WSH);
}
_ => panic!("Invalid parsed address"),
}

let wpkh = "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4";
let pox_addr = parse_pox_addr(wpkh).expect("Failed to parse segwit address");
match pox_addr {
PoxAddress::Addr20(_, addr_type, _) => {
assert_eq!(addr_type, PoxAddressType20::P2WPKH);
}
_ => panic!("Invalid parsed address"),
}
}
}
164 changes: 161 additions & 3 deletions stacks-signer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::sync::mpsc::{channel, Receiver, Sender};
use std::time::Duration;

use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
use blockstack_lib::util_lib::signed_structured_data::pox4::make_pox_4_signer_key_signature;
use clap::Parser;
use clarity::vm::types::QualifiedContractIdentifier;
use libsigner::{
Expand All @@ -44,10 +45,12 @@ use libstackerdb::StackerDBChunkData;
use slog::{slog_debug, slog_error};
use stacks_common::codec::read_next;
use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey};
use stacks_common::util::hash::to_hex;
use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey};
use stacks_common::{debug, error};
use stacks_signer::cli::{
Cli, Command, GenerateFilesArgs, GetChunkArgs, GetLatestChunkArgs, PutChunkArgs, RunDkgArgs,
SignArgs, StackerDBArgs,
Cli, Command, GenerateFilesArgs, GenerateStackingSignatureArgs, GetChunkArgs,
GetLatestChunkArgs, PutChunkArgs, RunDkgArgs, SignArgs, StackerDBArgs,
};
use stacks_signer::config::Config;
use stacks_signer::runloop::{RunLoop, RunLoopCommand};
Expand Down Expand Up @@ -307,6 +310,36 @@ fn handle_generate_files(args: GenerateFilesArgs) {
}
}

fn handle_generate_stacking_signature(
args: GenerateStackingSignatureArgs,
do_print: bool,
) -> MessageSignature {
let config = Config::try_from(&args.config).unwrap();

let private_key = config.stacks_private_key;
let public_key = Secp256k1PublicKey::from_private(&private_key);

let signature = make_pox_4_signer_key_signature(
&args.pox_address,
&private_key, //
args.reward_cycle.into(),
&args.method.topic(),
config.network.to_chain_id(),
args.period.into(),
)
.expect("Failed to generate signature");

if do_print {
println!(
"\nSigner Public Key: 0x{}\nSigner Key Signature: 0x{}\n\n",
to_hex(&public_key.to_bytes_compressed()),
to_hex(signature.to_rsv().as_slice()) // RSV is needed for Clarity
);
}

signature
}

/// Helper function for writing the given contents to filename in the given directory
fn write_file(dir: &Path, filename: &str, contents: &str) {
let file_path = dir.join(filename);
Expand Down Expand Up @@ -352,8 +385,133 @@ fn main() {
Command::GenerateFiles(args) => {
handle_generate_files(args);
}
Command::GenerateStackingSignature(args) => {
handle_generate_stacking_signature(args, true);
}
}
}

#[cfg(test)]
pub mod tests;
pub mod tests {
use blockstack_lib::{
chainstate::stacks::address::PoxAddress,
chainstate::stacks::boot::POX_4_CODE,
util_lib::signed_structured_data::pox4::{
make_pox_4_signer_key_message_hash, Pox4SignatureTopic,
},
};
use stacks_common::{
consts::CHAIN_ID_TESTNET, types::PublicKey, util::secp256k1::Secp256k1PublicKey,
};
use stacks_signer::cli::parse_pox_addr;

use super::handle_generate_stacking_signature;
use crate::{Config, GenerateStackingSignatureArgs};
use clarity::vm::{execute_v2, Value};

use super::*;

fn call_verify_signer_sig(
pox_addr: &PoxAddress,
reward_cycle: u128,
topic: &Pox4SignatureTopic,
lock_period: u128,
public_key: &Secp256k1PublicKey,
signature: Vec<u8>,
) -> bool {
let program = format!(
r#"
{}
(verify-signer-key-sig {} u{} "{}" u{} 0x{} 0x{})
"#,
&*POX_4_CODE, //s
Value::Tuple(pox_addr.clone().as_clarity_tuple().unwrap()), //p
reward_cycle,
topic.get_name_str(),
lock_period,
to_hex(signature.as_slice()),
to_hex(public_key.to_bytes_compressed().as_slice()),
);
let result = execute_v2(&program)
.expect("FATAL: could not execute program")
.expect("Expected result")
.expect_result_ok()
.expect("Expected ok result")
.expect_bool()
.expect("Expected buff");
result
}

#[test]
fn test_stacking_signature_with_pox_code() {
let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap();
let btc_address = "bc1p8vg588hldsnv4a558apet4e9ff3pr4awhqj2hy8gy6x2yxzjpmqsvvpta4";
let mut args = GenerateStackingSignatureArgs {
config: "./src/tests/conf/signer-0.toml".into(),
pox_address: parse_pox_addr(btc_address).unwrap(),
reward_cycle: 6,
method: Pox4SignatureTopic::StackStx.into(),
period: 12,
};

let signature = handle_generate_stacking_signature(args.clone(), false);
let public_key = Secp256k1PublicKey::from_private(&config.stacks_private_key);

let valid = call_verify_signer_sig(
&args.pox_address,
args.reward_cycle.into(),
&Pox4SignatureTopic::StackStx,
args.period.into(),
&public_key,
signature.to_rsv(),
);
assert!(valid);

// change up some args
args.period = 6;
args.method = Pox4SignatureTopic::AggregationCommit.into();
args.reward_cycle = 7;

let signature = handle_generate_stacking_signature(args.clone(), false);
let public_key = Secp256k1PublicKey::from_private(&config.stacks_private_key);

let valid = call_verify_signer_sig(
&args.pox_address,
args.reward_cycle.into(),
&Pox4SignatureTopic::AggregationCommit,
args.period.into(),
&public_key,
signature.to_rsv(),
);
assert!(valid);
}

#[test]
fn test_generate_stacking_signature() {
let config = Config::load_from_file("./src/tests/conf/signer-0.toml").unwrap();
let btc_address = "bc1p8vg588hldsnv4a558apet4e9ff3pr4awhqj2hy8gy6x2yxzjpmqsvvpta4";
let args = GenerateStackingSignatureArgs {
config: "./src/tests/conf/signer-0.toml".into(),
pox_address: parse_pox_addr(btc_address).unwrap(),
reward_cycle: 6,
method: Pox4SignatureTopic::StackStx.into(),
period: 12,
};

let signature = handle_generate_stacking_signature(args.clone(), false);

let public_key = Secp256k1PublicKey::from_private(&config.stacks_private_key);

let message_hash = make_pox_4_signer_key_message_hash(
&args.pox_address,
args.reward_cycle.into(),
&Pox4SignatureTopic::StackStx,
CHAIN_ID_TESTNET,
args.period.into(),
);

let verify_result = public_key.verify(&message_hash.0, &signature);
assert!(verify_result.is_ok());
assert!(verify_result.unwrap());
}
}
Loading

0 comments on commit 9690ecd

Please sign in to comment.