From 28ca0be781222d6071c960130f480f52b63ab6dc Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Mon, 27 Nov 2023 08:57:33 -0800 Subject: [PATCH 01/13] Threshold Custody: Add FollowerRound1 domain type From ee7c0abd26a171cc14271f7d21540a1652eb056a Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Mon, 27 Nov 2023 08:57:33 -0800 Subject: [PATCH 02/13] Threshold Custody: Add FollowerRound1 domain type From 424c95aa373c75233f8ee0d5e423df9e20262cd2 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 28 Nov 2023 16:14:25 -0800 Subject: [PATCH 03/13] Threshold Custody: make config fields private This allows us to easily restructure them. --- crates/custody/src/threshold.rs | 12 ++++----- crates/custody/src/threshold/config.rs | 36 +++++++++++++++++++++----- crates/custody/src/threshold/sign.rs | 18 ++++++------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/crates/custody/src/threshold.rs b/crates/custody/src/threshold.rs index 1ff4cd31db..54bbd985b4 100644 --- a/crates/custody/src/threshold.rs +++ b/crates/custody/src/threshold.rs @@ -139,13 +139,13 @@ impl Threshold { self.terminal .explain(&format!( "Now, gather at least {} replies from the other signers, and paste them below:", - self.config.threshold + self.config.threshold() )) .await?; let round1_replies = { let mut acc = Vec::new(); // We need 1 less, since we've already included ourselves. - for _ in 1..self.config.threshold { + for _ in 1..self.config.threshold() { let reply_str = self .terminal .next_response() @@ -171,7 +171,7 @@ impl Threshold { let round2_replies = { let mut acc = Vec::new(); // We need 1 less, since we've already included ourselves. - for _ in 1..self.config.threshold { + for _ in 1..self.config.threshold() { let reply_str = self .terminal .next_response() @@ -188,14 +188,14 @@ impl Threshold { /// Return the full viewing key. fn export_full_viewing_key(&self) -> FullViewingKey { - self.config.fvk.clone() + self.config.fvk().clone() } /// Get the address associated with an index. /// /// This is just to match the API of the custody trait. fn confirm_address(&self, index: AddressIndex) -> Address { - self.config.fvk.payment_address(index).0 + self.config.fvk().payment_address(index).0 } } @@ -355,7 +355,7 @@ mod test { tokio::spawn(async move { follow(&config, &terminal).await }); } let plan = serde_json::from_str::(TEST_PLAN)?; - let fvk = coordinator_config.fvk.clone(); + let fvk = coordinator_config.fvk().clone(); let authorization_data = Threshold::new(coordinator_config, coordinator_terminal) .authorize(AuthorizeRequest { plan: plan.clone(), diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index 0567240362..b22bdbcd38 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -9,12 +9,12 @@ use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone)] pub struct Config { - pub threshold: u16, - pub key_package: frost::keys::KeyPackage, - pub public_key_package: frost::keys::PublicKeyPackage, - pub signing_key: SigningKey, - pub fvk: FullViewingKey, - pub verification_keys: HashSet, + threshold: u16, + key_package: frost::keys::KeyPackage, + public_key_package: frost::keys::PublicKeyPackage, + signing_key: SigningKey, + fvk: FullViewingKey, + verification_keys: HashSet, } impl Config { @@ -76,4 +76,28 @@ impl Config { }) .collect()) } + + pub fn threshold(&self) -> u16 { + self.threshold + } + + pub fn key_package(&self) -> frost::keys::KeyPackage { + self.key_package.clone() + } + + pub fn public_key_package(&self) -> frost::keys::PublicKeyPackage { + self.public_key_package.clone() + } + + pub fn signing_key(&self) -> &SigningKey { + &self.signing_key + } + + pub fn fvk(&self) -> &FullViewingKey { + &self.fvk + } + + pub fn verification_keys(&self) -> HashSet { + self.verification_keys.clone() + } } diff --git a/crates/custody/src/threshold/sign.rs b/crates/custody/src/threshold/sign.rs index 3bc2e45234..455066b8bb 100644 --- a/crates/custody/src/threshold/sign.rs +++ b/crates/custody/src/threshold/sign.rs @@ -336,7 +336,7 @@ pub fn coordinator_round2( .chain(iter::once(state.my_round1_reply)) { let (pk, commitments) = message.checked_commitments()?; - if !config.verification_keys.contains(&pk) { + if !config.verification_keys().contains(&pk) { anyhow::bail!("unknown verification key: {:?}", pk); } // The public key acts as the identifier @@ -348,7 +348,7 @@ pub fn coordinator_round2( let reply = CoordinatorRound2 { all_commitments }; let my_round2_reply = follower_round2(config, state.my_round1_state, reply.clone())?; - let effect_hash = state.plan.effect_hash(&config.fvk); + let effect_hash = state.plan.effect_hash(config.fvk()); let signing_packages = { reply .all_commitments @@ -378,7 +378,7 @@ pub fn coordinator_round3( .chain(iter::once(state.my_round2_reply)) { let (pk, shares) = message.checked_shares()?; - if !config.verification_keys.contains(&pk) { + if !config.verification_keys().contains(&pk) { anyhow::bail!("unknown verification key: {:?}", pk); } let identifier = frost::Identifier::derive(pk.as_bytes().as_slice())?; @@ -397,7 +397,7 @@ pub fn coordinator_round3( frost::aggregate_randomized( signing_package, &share_map, - &config.public_key_package, + &config.public_key_package(), randomizer, ) }) @@ -417,9 +417,9 @@ pub fn follower_round1( ) -> Result<(FollowerRound1, FollowerState)> { let required = required_signatures(&coordinator.plan); let (nonces, commitments) = (0..required) - .map(|_| frost::round1::commit(&config.key_package.secret_share(), rng)) + .map(|_| frost::round1::commit(&config.key_package().secret_share(), rng)) .unzip(); - let reply = FollowerRound1::make(&config.signing_key, commitments); + let reply = FollowerRound1::make(config.signing_key(), commitments); let state = FollowerState { plan: coordinator.plan, nonces, @@ -432,7 +432,7 @@ pub fn follower_round2( state: FollowerState, coordinator: CoordinatorRound2, ) -> Result { - let effect_hash = state.plan.effect_hash(&config.fvk); + let effect_hash = state.plan.effect_hash(config.fvk()); let signing_packages = coordinator .all_commitments .into_iter() @@ -448,10 +448,10 @@ pub fn follower_round2( frost::round2::sign_randomized( &signing_package, &signer_nonces, - &config.key_package, + &config.key_package(), randomizer, ) }) .collect::>()?; - Ok(FollowerRound2::make(&config.signing_key, shares)) + Ok(FollowerRound2::make(config.signing_key(), shares)) } From 0f8958ecb272e50851e0da70883eeb955abbeab8 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Tue, 28 Nov 2023 17:03:56 -0800 Subject: [PATCH 04/13] Threshold Custody: Minimize config struct size This also makes implementing the serialization much easier. --- crates/crypto/decaf377-frost/src/keys.rs | 3 ++ crates/custody/src/threshold/config.rs | 62 +++++++++++++++++------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/crypto/decaf377-frost/src/keys.rs b/crates/crypto/decaf377-frost/src/keys.rs index bef607c062..7154d3f2e9 100644 --- a/crates/crypto/decaf377-frost/src/keys.rs +++ b/crates/crypto/decaf377-frost/src/keys.rs @@ -72,6 +72,9 @@ pub type SigningShare = frost::keys::SigningShare; /// A public group element that represents a single signer's public verification share. pub type VerifyingShare = frost::keys::VerifyingShare; +/// A valid verifying key for Schnorr signatures over a FROST [`Ciphersuite::Group`]. +pub type VerifyingKey = frost_core::VerifyingKey; + /// A FROST keypair, which can be generated either by a trusted dealer or using a DKG. /// /// When using a central dealer, [`SecretShare`]s are distributed to diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index b22bdbcd38..413cce8cc3 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -10,11 +10,10 @@ use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone)] pub struct Config { threshold: u16, - key_package: frost::keys::KeyPackage, - public_key_package: frost::keys::PublicKeyPackage, - signing_key: SigningKey, fvk: FullViewingKey, - verification_keys: HashSet, + spend_key_share: frost::keys::SigningShare, + signing_key: SigningKey, + verifying_shares: HashMap, } impl Config { @@ -40,7 +39,14 @@ impl Config { ), &mut rng, )?; - let verification_keys = signing_keys.keys().cloned().collect::>(); + let verifying_shares = signing_keys + .keys() + .map(|pk| { + let identifier = frost::Identifier::derive(pk.to_bytes().as_slice()) + .expect("should be able to derive identifier"); + (pk.clone(), public_key_package.signer_pubkeys()[&identifier]) + }) + .collect::>(); // Okay, this conversion is a bit of a hack, but it should work... // It's a hack cause we're going via the serialization, but, you know, that should be fine. let fvk = FullViewingKey::from_components( @@ -58,20 +64,12 @@ impl Config { .map(|(verification_key, signing_key)| { let identifier = identifiers[&verification_key]; let signing_share = share_map[&identifier].value().clone(); - let key_package = frost::keys::KeyPackage::new( - identifier, - signing_share, - signing_share.into(), - public_key_package.group_public().clone(), - t, - ); Self { threshold: t, - key_package, - public_key_package: public_key_package.clone(), signing_key, fvk: fvk.clone(), - verification_keys: verification_keys.clone(), + spend_key_share: signing_share, + verifying_shares: verifying_shares.clone(), } }) .collect()) @@ -81,12 +79,40 @@ impl Config { self.threshold } + fn group_public(&self) -> frost::keys::VerifyingKey { + frost::keys::VerifyingKey::deserialize( + self.fvk.spend_verification_key().to_bytes().to_vec(), + ) + .expect("should be able to parse out VerifyingKey from FullViewingKey") + } + pub fn key_package(&self) -> frost::keys::KeyPackage { - self.key_package.clone() + let identifier = + frost::Identifier::derive(&self.signing_key.verification_key().as_bytes().as_slice()) + .expect("deriving our identifier should not fail"); + + frost::keys::KeyPackage::new( + identifier, + self.spend_key_share, + self.spend_key_share.into(), + self.group_public(), + self.threshold, + ) } pub fn public_key_package(&self) -> frost::keys::PublicKeyPackage { - self.public_key_package.clone() + let signer_pubkeys = self + .verifying_shares + .iter() + .map(|(vk, share)| { + ( + frost::Identifier::derive(vk.to_bytes().as_slice()) + .expect("deriving an identifier should not fail"), + share.clone(), + ) + }) + .collect(); + frost::keys::PublicKeyPackage::new(signer_pubkeys, self.group_public()) } pub fn signing_key(&self) -> &SigningKey { @@ -98,6 +124,6 @@ impl Config { } pub fn verification_keys(&self) -> HashSet { - self.verification_keys.clone() + self.verifying_shares.keys().cloned().collect() } } From 12882fdc73c9c57d606c9cb4deed7dfd5fe6fef5 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 08:26:16 -0800 Subject: [PATCH 05/13] Threshold Custody: implement serialization for Config --- crates/custody/src/threshold/config.rs | 65 +++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index 413cce8cc3..2263232899 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -5,14 +5,56 @@ use decaf377_frost as frost; use ed25519_consensus::{SigningKey, VerificationKey}; use penumbra_keys::{keys::NullifierKey, FullViewingKey}; use rand_core::CryptoRngCore; +use serde::{Deserialize, Serialize}; +use serde_with::{DisplayFromStr, Seq, TryFromInto}; use std::collections::{HashMap, HashSet}; -#[derive(Debug, Clone)] +/// A shim to serialize frost::keys::SigningShare +#[derive(Serialize, Deserialize)] +struct SigningShareWrapper(Vec); + +impl From for SigningShareWrapper { + fn from(value: frost::keys::SigningShare) -> Self { + Self(value.serialize()) + } +} + +impl TryFrom for frost::keys::SigningShare { + type Error = anyhow::Error; + + fn try_from(value: SigningShareWrapper) -> std::result::Result { + Ok(Self::deserialize(value.0)?) + } +} + +/// A shim to serialize frost::keys::VerifyingShare +#[derive(Serialize, Deserialize)] +struct VerifyingShareWrapper(Vec); + +impl From for VerifyingShareWrapper { + fn from(value: frost::keys::VerifyingShare) -> Self { + Self(value.serialize()) + } +} + +impl TryFrom for frost::keys::VerifyingShare { + type Error = anyhow::Error; + + fn try_from(value: VerifyingShareWrapper) -> std::result::Result { + Ok(Self::deserialize(value.0)?) + } +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { threshold: u16, + #[serde_as(as = "DisplayFromStr")] fvk: FullViewingKey, + #[serde_as(as = "TryFromInto")] spend_key_share: frost::keys::SigningShare, signing_key: SigningKey, + #[serde_as(as = "Seq<(_, TryFromInto)>")] verifying_shares: HashMap, } @@ -127,3 +169,24 @@ impl Config { self.verifying_shares.keys().cloned().collect() } } + +#[cfg(test)] +mod test { + use rand_core::OsRng; + + use super::*; + + #[test] + fn test_config_serialization_roundtrip() -> Result<()> { + // You can't put 1, because no FUN is allowed + let config = Config::deal(&mut OsRng, 2, 2)?.pop().unwrap(); + let config_str = serde_json::to_string(&config)?; + let config2: Config = serde_json::from_str(&config_str)?; + // Can't derive partial eq, so go field by field + assert_eq!(config.threshold, config2.threshold); + assert_eq!(config.fvk, config2.fvk); + assert_eq!(config.spend_key_share, config2.spend_key_share); + assert_eq!(config.verifying_shares, config2.verifying_shares); + Ok(()) + } +} From a4967baaa1b31dcf2cc39b116b4c25fc2c61b412 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 09:00:38 -0800 Subject: [PATCH 06/13] pcli: add threshold custody deal command Also adds a stub terminal impl, which we'll fill in a bit. --- crates/bin/pcli/src/command/init.rs | 50 ++++++++++++++++++++++++++ crates/bin/pcli/src/config.rs | 4 ++- crates/bin/pcli/src/main.rs | 1 + crates/bin/pcli/src/opt.rs | 8 +++++ crates/bin/pcli/src/terminal.rs | 28 +++++++++++++++ crates/custody/src/threshold/config.rs | 13 +++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 crates/bin/pcli/src/terminal.rs diff --git a/crates/bin/pcli/src/command/init.rs b/crates/bin/pcli/src/command/init.rs index eb3535b530..2f6c88ba86 100644 --- a/crates/bin/pcli/src/command/init.rs +++ b/crates/bin/pcli/src/command/init.rs @@ -1,6 +1,7 @@ use std::{io::Read, str::FromStr}; use anyhow::Result; +use camino::Utf8PathBuf; use penumbra_keys::keys::{Bip44Path, SeedPhrase, SpendKey}; use rand_core::OsRng; use url::Url; @@ -30,6 +31,9 @@ pub enum InitSubCmd { /// Initialize `pcli` with a basic, file-based custody backend. #[clap(subcommand, display_order = 100)] SoftKms(SoftKmsInitCmd), + /// Initialize `pcli` with a manbual threshold signing backend. + #[clap(subcommand, display_order = 150)] + Threshold(ThresholdInitCmd), /// Initialize `pcli` in view-only mode, without spending keys. #[clap(display_order = 200)] ViewOnly { @@ -106,8 +110,53 @@ impl SoftKmsInitCmd { } } +#[derive(Debug, clap::Subcommand)] +pub enum ThresholdInitCmd { + /// Use a centralized dealer to create config files for each signer. + Deal { + /// The minimum number of signers required to make a signature (>= 2). + #[clap(short, long)] + threshold: u16, + /// One file name for each signer, where the dealt config will be saved. + #[clap(short, long, value_delimiter = ' ', multiple_values = true)] + out: Vec, + }, +} + +impl ThresholdInitCmd { + fn exec(&self, grpc_url: Url) -> Result<()> { + use penumbra_custody::threshold; + + match self { + ThresholdInitCmd::Deal { threshold: t, out } => { + if *t < 2 { + anyhow::bail!("threshold must be >= 2"); + } + let configs = threshold::Config::deal(&mut OsRng, *t, out.len() as u16)?; + println!("Writing dealt config files."); + for (config, path) in configs.into_iter().zip(out.iter()) { + let full_viewing_key = config.fvk().clone(); + let config = PcliConfig { + custody: CustodyConfig::Threshold(config), + full_viewing_key, + grpc_url: grpc_url.clone(), + view_url: None, + disable_warning: false, + }; + config.save(path)?; + } + Ok(()) + } + } + } +} + impl InitCmd { pub fn exec(&self, home_dir: impl AsRef) -> Result<()> { + if let InitSubCmd::Threshold(cmd) = &self.subcmd { + cmd.exec(self.grpc_url.clone())?; + return Ok(()); + } let home_dir = home_dir.as_ref(); match &self.subcmd { @@ -136,6 +185,7 @@ impl InitCmd { CustodyConfig::SoftKms(spend_key.into()), ) } + InitSubCmd::Threshold(_) => panic!("this should already have been handled above"), InitSubCmd::ViewOnly { full_viewing_key } => { let full_viewing_key = full_viewing_key.parse()?; (full_viewing_key, CustodyConfig::ViewOnly) diff --git a/crates/bin/pcli/src/config.rs b/crates/bin/pcli/src/config.rs index 9de3748ec4..5efb87bfd0 100644 --- a/crates/bin/pcli/src/config.rs +++ b/crates/bin/pcli/src/config.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use url::Url; -use penumbra_custody::soft_kms::Config as SoftKmsConfig; +use penumbra_custody::{soft_kms::Config as SoftKmsConfig, threshold::Config as ThresholdConfig}; use penumbra_keys::FullViewingKey; /// Configuration data for `pcli`. @@ -51,6 +51,8 @@ pub enum CustodyConfig { ViewOnly, /// A software key management service. SoftKms(SoftKmsConfig), + /// A manual threshold custody service. + Threshold(ThresholdConfig), } impl Default for CustodyConfig { diff --git a/crates/bin/pcli/src/main.rs b/crates/bin/pcli/src/main.rs index a5259b5a94..dda546f1a9 100644 --- a/crates/bin/pcli/src/main.rs +++ b/crates/bin/pcli/src/main.rs @@ -18,6 +18,7 @@ mod config; mod dex_utils; mod network; mod opt; +mod terminal; mod warning; use opt::Opt; diff --git a/crates/bin/pcli/src/opt.rs b/crates/bin/pcli/src/opt.rs index 1731cf2bdc..41adc64e07 100644 --- a/crates/bin/pcli/src/opt.rs +++ b/crates/bin/pcli/src/opt.rs @@ -1,6 +1,7 @@ use crate::{ box_grpc_svc, config::{CustodyConfig, PcliConfig}, + terminal::ActualTerminal, App, Command, }; use anyhow::Result; @@ -70,6 +71,13 @@ impl Opt { let custody_svc = CustodyProtocolServiceServer::new(soft_kms); CustodyProtocolServiceClient::new(box_grpc_svc::local(custody_svc)) } + CustodyConfig::Threshold(config) => { + tracing::info!("using manual threshold custody service"); + let threshold_kms = + penumbra_custody::threshold::Threshold::new(config.clone(), ActualTerminal); + let custody_svc = CustodyProtocolServiceServer::new(threshold_kms); + CustodyProtocolServiceClient::new(box_grpc_svc::local(custody_svc)) + } }; // ...and the view service... diff --git a/crates/bin/pcli/src/terminal.rs b/crates/bin/pcli/src/terminal.rs new file mode 100644 index 0000000000..a9cfbf6580 --- /dev/null +++ b/crates/bin/pcli/src/terminal.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use penumbra_custody::threshold::Terminal; +use penumbra_transaction::plan::TransactionPlan; +use tonic::async_trait; + +/// For threshold custody, we need to implement this weird terminal abstraction. +/// +/// This actually does stuff to stdin and stdout. +pub struct ActualTerminal; + +#[async_trait] +impl Terminal for ActualTerminal { + async fn confirm_transaction(&self, _transaction: &TransactionPlan) -> Result { + todo!() + } + + async fn explain(&self, _msg: &str) -> Result<()> { + todo!() + } + + async fn broadcast(&self, _data: &str) -> Result<()> { + todo!() + } + + async fn next_response(&self) -> Result> { + todo!() + } +} diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index 2263232899..b244578a80 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -58,6 +58,19 @@ pub struct Config { verifying_shares: HashMap, } +impl PartialEq for Config { + fn eq(&self, other: &Self) -> bool { + self.threshold == other.threshold + && self.fvk == other.fvk + && self.spend_key_share == other.spend_key_share + // TIMING LEAK + && self.signing_key.as_bytes() == other.signing_key.as_bytes() + && self.verifying_shares == other.verifying_shares + } +} + +impl Eq for Config {} + impl Config { pub fn deal(mut rng: &mut impl CryptoRngCore, t: u16, n: u16) -> Result> { let signing_keys = (0..n) From a8736f8d46e0808f7f93756d775aed869a79ae59 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 09:36:48 -0800 Subject: [PATCH 07/13] Threshold Custody: use nicer config serialization This makes the config file not look absolutely stupid --- crates/custody/src/threshold/config.rs | 51 ++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index b244578a80..76269a4386 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -6,12 +6,13 @@ use ed25519_consensus::{SigningKey, VerificationKey}; use penumbra_keys::{keys::NullifierKey, FullViewingKey}; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; -use serde_with::{DisplayFromStr, Seq, TryFromInto}; +use serde_with::{formats::Uppercase, hex::Hex, DisplayFromStr, Seq, TryFromInto}; use std::collections::{HashMap, HashSet}; /// A shim to serialize frost::keys::SigningShare +#[serde_as] #[derive(Serialize, Deserialize)] -struct SigningShareWrapper(Vec); +struct SigningShareWrapper(#[serde_as(as = "Hex")] Vec); impl From for SigningShareWrapper { fn from(value: frost::keys::SigningShare) -> Self { @@ -28,8 +29,9 @@ impl TryFrom for frost::keys::SigningShare { } /// A shim to serialize frost::keys::VerifyingShare +#[serde_as] #[derive(Serialize, Deserialize)] -struct VerifyingShareWrapper(Vec); +struct VerifyingShareWrapper(#[serde_as(as = "Hex")] Vec); impl From for VerifyingShareWrapper { fn from(value: frost::keys::VerifyingShare) -> Self { @@ -45,6 +47,44 @@ impl TryFrom for frost::keys::VerifyingShare { } } +/// A shim to serialize SigningKey +#[serde_as] +#[derive(Serialize, Deserialize)] +struct SigningKeyWrapper(#[serde_as(as = "Hex")] Vec); + +impl From for SigningKeyWrapper { + fn from(value: SigningKey) -> Self { + Self(value.to_bytes().to_vec()) + } +} + +impl TryFrom for SigningKey { + type Error = anyhow::Error; + + fn try_from(value: SigningKeyWrapper) -> std::result::Result { + Ok(Self::try_from(value.0.as_slice())?) + } +} + +/// A shim to serialize VerifyingKey +#[serde_as] +#[derive(Serialize, Deserialize)] +struct VerificationKeyWrapper(#[serde_as(as = "Hex")] Vec); + +impl From for VerificationKeyWrapper { + fn from(value: VerificationKey) -> Self { + Self(value.to_bytes().to_vec()) + } +} + +impl TryFrom for VerificationKey { + type Error = anyhow::Error; + + fn try_from(value: VerificationKeyWrapper) -> std::result::Result { + Ok(Self::try_from(value.0.as_slice())?) + } +} + #[serde_as] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { @@ -53,8 +93,11 @@ pub struct Config { fvk: FullViewingKey, #[serde_as(as = "TryFromInto")] spend_key_share: frost::keys::SigningShare, + #[serde_as(as = "TryFromInto")] signing_key: SigningKey, - #[serde_as(as = "Seq<(_, TryFromInto)>")] + #[serde_as( + as = "HashMap, TryFromInto>" + )] verifying_shares: HashMap, } From a2a5c1af618ec26fe288e9b5ae618f16be265fb2 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 09:44:35 -0800 Subject: [PATCH 08/13] pcli: implement terminal abstraction for threshold custody --- crates/bin/pcli/src/terminal.rs | 30 ++++++++++++++++++++------ crates/custody/src/threshold/config.rs | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/bin/pcli/src/terminal.rs b/crates/bin/pcli/src/terminal.rs index a9cfbf6580..9ad642758d 100644 --- a/crates/bin/pcli/src/terminal.rs +++ b/crates/bin/pcli/src/terminal.rs @@ -1,6 +1,7 @@ use anyhow::Result; use penumbra_custody::threshold::Terminal; use penumbra_transaction::plan::TransactionPlan; +use tokio::io::{self, AsyncBufReadExt}; use tonic::async_trait; /// For threshold custody, we need to implement this weird terminal abstraction. @@ -10,19 +11,34 @@ pub struct ActualTerminal; #[async_trait] impl Terminal for ActualTerminal { - async fn confirm_transaction(&self, _transaction: &TransactionPlan) -> Result { - todo!() + async fn confirm_transaction(&self, transaction: &TransactionPlan) -> Result { + println!("Do you approve this transaction?"); + println!("{}", serde_json::to_string_pretty(transaction)?); + println!("Y/N?"); + let response = self.next_response().await?; + Ok(response.map(|x| x.to_lowercase() == "y").unwrap_or(false)) } - async fn explain(&self, _msg: &str) -> Result<()> { - todo!() + async fn explain(&self, msg: &str) -> Result<()> { + println!("{}", msg); + Ok(()) } - async fn broadcast(&self, _data: &str) -> Result<()> { - todo!() + async fn broadcast(&self, data: &str) -> Result<()> { + println!("{}", data); + Ok(()) } async fn next_response(&self) -> Result> { - todo!() + let stdin = io::stdin(); + let mut stdin = io::BufReader::new(stdin); + + let mut line = String::new(); + stdin.read_line(&mut line).await?; + + if line.is_empty() { + return Ok(None); + } + Ok(Some(line)) } } diff --git a/crates/custody/src/threshold/config.rs b/crates/custody/src/threshold/config.rs index 76269a4386..9cee6bb7d5 100644 --- a/crates/custody/src/threshold/config.rs +++ b/crates/custody/src/threshold/config.rs @@ -6,7 +6,7 @@ use ed25519_consensus::{SigningKey, VerificationKey}; use penumbra_keys::{keys::NullifierKey, FullViewingKey}; use rand_core::CryptoRngCore; use serde::{Deserialize, Serialize}; -use serde_with::{formats::Uppercase, hex::Hex, DisplayFromStr, Seq, TryFromInto}; +use serde_with::{formats::Uppercase, hex::Hex, DisplayFromStr, TryFromInto}; use std::collections::{HashMap, HashSet}; /// A shim to serialize frost::keys::SigningShare From 0172442360053f0ce3005408528844e8ceaa738d Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 10:40:58 -0800 Subject: [PATCH 09/13] pcli: add threshold follow commands Now threshold signing works with dealing --- crates/bin/pcli/src/command.rs | 6 ++++++ crates/bin/pcli/src/command/threshold.rs | 26 ++++++++++++++++++++++++ crates/bin/pcli/src/main.rs | 1 + crates/bin/pcli/src/terminal.rs | 6 +++--- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 crates/bin/pcli/src/command/threshold.rs diff --git a/crates/bin/pcli/src/command.rs b/crates/bin/pcli/src/command.rs index a7946e6605..e8a39abe70 100644 --- a/crates/bin/pcli/src/command.rs +++ b/crates/bin/pcli/src/command.rs @@ -6,10 +6,12 @@ mod tx; mod utils; mod validator; mod view; +mod threshold; pub use debug::DebugCmd; pub use init::InitCmd; pub use query::QueryCmd; +pub use threshold::ThresholdCmd; pub use tx::TxCmd; pub use validator::ValidatorCmd; pub use view::transaction_hashes::TransactionHashesCmd; @@ -61,6 +63,9 @@ pub enum Command { /// Contribute to the summoning ceremony. #[clap(subcommand, display_order = 990)] Ceremony(CeremonyCmd), + /// Follow the threshold signing protocol. + #[clap(subcommand, display_order = 500)] + Threshold(ThresholdCmd), } impl Command { @@ -74,6 +79,7 @@ impl Command { Command::Query(cmd) => cmd.offline(), Command::Debug(cmd) => cmd.offline(), Command::Ceremony(_) => false, + Command::Threshold(cmd) => cmd.offline(), } } } diff --git a/crates/bin/pcli/src/command/threshold.rs b/crates/bin/pcli/src/command/threshold.rs new file mode 100644 index 0000000000..6bbbb42b38 --- /dev/null +++ b/crates/bin/pcli/src/command/threshold.rs @@ -0,0 +1,26 @@ +use anyhow::Result; + +use crate::{terminal::ActualTerminal, App}; + +#[derive(Debug, clap::Subcommand)] +pub enum ThresholdCmd { + /// Follow along with the threshold signing process + Follow, +} + +impl ThresholdCmd { + pub fn offline(&self) -> bool { + match self { + ThresholdCmd::Follow => true, + } + } + + #[tracing::instrument(skip(self, app))] + pub async fn exec(&self, app: &mut App) -> Result<()> { + let config = match &app.config.custody { + crate::config::CustodyConfig::Threshold(config) => config, + _ => anyhow::bail!("this command can only be used with the threshold custody backend"), + }; + penumbra_custody::threshold::follow(config, &ActualTerminal).await + } +} diff --git a/crates/bin/pcli/src/main.rs b/crates/bin/pcli/src/main.rs index dda546f1a9..083b50ad55 100644 --- a/crates/bin/pcli/src/main.rs +++ b/crates/bin/pcli/src/main.rs @@ -140,6 +140,7 @@ async fn main() -> Result<()> { Command::Validator(cmd) => cmd.exec(&mut app).await?, Command::Query(cmd) => cmd.exec(&mut app).await?, Command::Ceremony(cmd) => cmd.exec(&mut app).await?, + Command::Threshold(cmd) => cmd.exec(&mut app).await?, } Ok(()) diff --git a/crates/bin/pcli/src/terminal.rs b/crates/bin/pcli/src/terminal.rs index 9ad642758d..39e59bfcb9 100644 --- a/crates/bin/pcli/src/terminal.rs +++ b/crates/bin/pcli/src/terminal.rs @@ -14,9 +14,9 @@ impl Terminal for ActualTerminal { async fn confirm_transaction(&self, transaction: &TransactionPlan) -> Result { println!("Do you approve this transaction?"); println!("{}", serde_json::to_string_pretty(transaction)?); - println!("Y/N?"); - let response = self.next_response().await?; - Ok(response.map(|x| x.to_lowercase() == "y").unwrap_or(false)) + println!("Press enter to continue"); + self.next_response().await?; + Ok(true) } async fn explain(&self, msg: &str) -> Result<()> { From 2be490ad45bbb24a721283c5b349f6835a2c2812 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Wed, 29 Nov 2023 10:46:51 -0800 Subject: [PATCH 10/13] rustfmt --- crates/bin/pcli/src/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bin/pcli/src/command.rs b/crates/bin/pcli/src/command.rs index e8a39abe70..f6d5611268 100644 --- a/crates/bin/pcli/src/command.rs +++ b/crates/bin/pcli/src/command.rs @@ -2,11 +2,11 @@ mod ceremony; mod debug; mod init; mod query; +mod threshold; mod tx; mod utils; mod validator; mod view; -mod threshold; pub use debug::DebugCmd; pub use init::InitCmd; From 18d3489c988ec475837790520dac75ad47c19852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BAc=C3=A1s=20Meier?= Date: Thu, 30 Nov 2023 15:30:12 -0800 Subject: [PATCH 11/13] Update crates/bin/pcli/src/command/init.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: redshiftzero Signed-off-by: Lúcás Meier --- crates/bin/pcli/src/command/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bin/pcli/src/command/init.rs b/crates/bin/pcli/src/command/init.rs index 2f6c88ba86..020c54f020 100644 --- a/crates/bin/pcli/src/command/init.rs +++ b/crates/bin/pcli/src/command/init.rs @@ -31,7 +31,7 @@ pub enum InitSubCmd { /// Initialize `pcli` with a basic, file-based custody backend. #[clap(subcommand, display_order = 100)] SoftKms(SoftKmsInitCmd), - /// Initialize `pcli` with a manbual threshold signing backend. + /// Initialize `pcli` with a manual threshold signing backend. #[clap(subcommand, display_order = 150)] Threshold(ThresholdInitCmd), /// Initialize `pcli` in view-only mode, without spending keys. From eaa2e01845d1efabd7e070a3ebb90d0f410fb671 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Fri, 1 Dec 2023 11:42:15 -0800 Subject: [PATCH 12/13] pcli: rename threshold follow to threshold sign --- crates/bin/pcli/src/command/threshold.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/bin/pcli/src/command/threshold.rs b/crates/bin/pcli/src/command/threshold.rs index 6bbbb42b38..a7923acf05 100644 --- a/crates/bin/pcli/src/command/threshold.rs +++ b/crates/bin/pcli/src/command/threshold.rs @@ -4,14 +4,14 @@ use crate::{terminal::ActualTerminal, App}; #[derive(Debug, clap::Subcommand)] pub enum ThresholdCmd { - /// Follow along with the threshold signing process - Follow, + /// Contribute to signing a transaction with threshold custody + Sign, } impl ThresholdCmd { pub fn offline(&self) -> bool { match self { - ThresholdCmd::Follow => true, + ThresholdCmd::Sign => true, } } @@ -21,6 +21,10 @@ impl ThresholdCmd { crate::config::CustodyConfig::Threshold(config) => config, _ => anyhow::bail!("this command can only be used with the threshold custody backend"), }; - penumbra_custody::threshold::follow(config, &ActualTerminal).await + match self { + ThresholdCmd::Sign => { + penumbra_custody::threshold::follow(config, &ActualTerminal).await + } + } } } From a03b25462bacaed734b116b83fc35e02efb15a18 Mon Sep 17 00:00:00 2001 From: Lucas Meier Date: Fri, 1 Dec 2023 14:10:01 -0800 Subject: [PATCH 13/13] pcli: use directories instead of files for threshold deal --- crates/bin/pcli/src/command/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bin/pcli/src/command/init.rs b/crates/bin/pcli/src/command/init.rs index 020c54f020..4a5b274b7f 100644 --- a/crates/bin/pcli/src/command/init.rs +++ b/crates/bin/pcli/src/command/init.rs @@ -143,7 +143,7 @@ impl ThresholdInitCmd { view_url: None, disable_warning: false, }; - config.save(path)?; + config.save(path.join(crate::CONFIG_FILE_NAME))?; } Ok(()) }