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 threshold custody commands to pcli #3449

Merged
merged 13 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from 10 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
6 changes: 6 additions & 0 deletions crates/bin/pcli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod ceremony;
mod debug;
mod init;
mod query;
mod threshold;
mod tx;
mod utils;
mod validator;
Expand All @@ -10,6 +11,7 @@ mod view;
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;
Expand Down Expand Up @@ -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 {
Expand All @@ -74,6 +79,7 @@ impl Command {
Command::Query(cmd) => cmd.offline(),
Command::Debug(cmd) => cmd.offline(),
Command::Ceremony(_) => false,
Command::Threshold(cmd) => cmd.offline(),
}
}
}
50 changes: 50 additions & 0 deletions crates/bin/pcli/src/command/init.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
cronokirby marked this conversation as resolved.
Show resolved Hide resolved
#[clap(subcommand, display_order = 150)]
Threshold(ThresholdInitCmd),
/// Initialize `pcli` in view-only mode, without spending keys.
#[clap(display_order = 200)]
ViewOnly {
Expand Down Expand Up @@ -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<Utf8PathBuf>,
cronokirby marked this conversation as resolved.
Show resolved Hide resolved
},
}

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<camino::Utf8Path>) -> Result<()> {
if let InitSubCmd::Threshold(cmd) = &self.subcmd {
cmd.exec(self.grpc_url.clone())?;
return Ok(());
cronokirby marked this conversation as resolved.
Show resolved Hide resolved
}
let home_dir = home_dir.as_ref();

match &self.subcmd {
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions crates/bin/pcli/src/command/threshold.rs
Original file line number Diff line number Diff line change
@@ -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,
cronokirby marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
}
4 changes: 3 additions & 1 deletion crates/bin/pcli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/bin/pcli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod config;
mod dex_utils;
mod network;
mod opt;
mod terminal;
mod warning;

use opt::Opt;
Expand Down Expand Up @@ -139,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(())
Expand Down
8 changes: 8 additions & 0 deletions crates/bin/pcli/src/opt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
box_grpc_svc,
config::{CustodyConfig, PcliConfig},
terminal::ActualTerminal,
App, Command,
};
use anyhow::Result;
Expand Down Expand Up @@ -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...
Expand Down
44 changes: 44 additions & 0 deletions crates/bin/pcli/src/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.
///
/// 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<bool> {
println!("Do you approve this transaction?");
println!("{}", serde_json::to_string_pretty(transaction)?);
println!("Press enter to continue");
self.next_response().await?;
Ok(true)
}

async fn explain(&self, msg: &str) -> Result<()> {
println!("{}", msg);
Ok(())
}

async fn broadcast(&self, data: &str) -> Result<()> {
println!("{}", data);
Ok(())
}

async fn next_response(&self) -> Result<Option<String>> {
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))
}
}
3 changes: 3 additions & 0 deletions crates/crypto/decaf377-frost/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ pub type SigningShare = frost::keys::SigningShare<E>;
/// A public group element that represents a single signer's public verification share.
pub type VerifyingShare = frost::keys::VerifyingShare<E>;

/// A valid verifying key for Schnorr signatures over a FROST [`Ciphersuite::Group`].
pub type VerifyingKey = frost_core::VerifyingKey<E>;

/// 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
Expand Down
12 changes: 6 additions & 6 deletions crates/custody/src/threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ impl<T: Terminal> Threshold<T> {
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()
Expand All @@ -171,7 +171,7 @@ impl<T: Terminal> Threshold<T> {
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()
Expand All @@ -188,14 +188,14 @@ impl<T: Terminal> Threshold<T> {

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

Expand Down Expand Up @@ -355,7 +355,7 @@ mod test {
tokio::spawn(async move { follow(&config, &terminal).await });
}
let plan = serde_json::from_str::<TransactionPlan>(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(),
Expand Down
Loading
Loading