Phase 2: add coordinator (#157)

* phase 2: add coordinator

* completed, but untested

* fix bug

* make it work for both ciphersuites

* allow generating fresh randomizer

* remove test for now
This commit is contained in:
Conrado Gouvea 2024-03-22 11:27:52 -03:00 committed by GitHub
parent 07ea1a1f66
commit 0ecf022bd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 238 additions and 16 deletions

5
Cargo.lock generated
View File

@ -408,7 +408,9 @@ dependencies = [
"message-io",
"rand",
"reddsa",
"reqwest",
"serde_json",
"server",
"thiserror",
"tokio",
]
@ -1891,10 +1893,13 @@ dependencies = [
"axum",
"axum-test",
"clap",
"coordinator",
"derivative",
"eyre",
"frost-ed25519",
"rand",
"reddsa",
"regex",
"reqwest",
"serde",
"serde_json",

View File

@ -17,9 +17,11 @@ serde_json = "1.0"
itertools = "0.12.1"
exitcode = "1.1.2"
clap = { version = "4.5.2", features = ["derive"] }
reqwest = { version = "0.11.23", features = ["json"] }
server = { path = "../server" }
tokio = { version = "1", features = ["full"] }
message-io = "0.18"
[features]
redpallas = []
redpallas = ["server/redpallas"]
default = []

View File

@ -9,6 +9,11 @@ pub struct Args {
#[arg(long, default_value_t = false)]
pub cli: bool,
/// HTTP mode. If enabled, it will use HTTP communication with a
/// FROST server.
#[arg(long, default_value_t = false)]
pub http: bool,
/// The number of participants. If 0, will prompt for a value.
#[arg(short = 'n', long, default_value_t = 0)]
pub num_signers: u16,
@ -24,8 +29,9 @@ pub struct Args {
#[arg(short = 'm', long, default_value = "")]
pub message: String,
/// The randomizer to use. Can be a file with the raw randomizer, or "-". If "-"
/// is specified, then it will be read from standard input as a hex string.
/// The randomizer to use. Can be a file with the raw randomizer, empty, or
/// "-". If empty, a random one will be generated. If "-" is specified, then
/// it will be read from standard input as a hex string.
#[cfg(feature = "redpallas")]
#[arg(short = 'r', long, default_value = "")]
pub randomizer: String,

View File

@ -2,6 +2,7 @@ use std::io::{BufRead, Write};
use crate::args::Args;
use crate::comms::cli::CLIComms;
use crate::comms::http::HTTPComms;
use crate::comms::socket::SocketComms;
use crate::comms::Comms;
use crate::step_1::step_1;
@ -20,6 +21,8 @@ pub async fn cli(
let mut comms: Box<dyn Comms> = if args.cli {
Box::new(CLIComms {})
} else if args.http {
Box::new(HTTPComms::new(args))
} else {
Box::new(SocketComms::new(args))
};
@ -39,7 +42,7 @@ pub async fn cli(
)?;
#[cfg(feature = "redpallas")]
let randomizer = request_randomizer(args, reader, logger)?;
let randomizer = request_randomizer(args, reader, logger, &signing_package)?;
writeln!(logger, "=== STEP 3: BUILD GROUP SIGNATURE ===\n")?;

View File

@ -1,4 +1,5 @@
pub mod cli;
pub mod http;
pub mod socket;
#[cfg(not(feature = "redpallas"))]

View File

@ -0,0 +1,147 @@
//! HTTP implementation of the Comms trait.
use async_trait::async_trait;
#[cfg(not(feature = "redpallas"))]
use frost_ed25519 as frost;
#[cfg(feature = "redpallas")]
use reddsa::frost::redpallas as frost;
use eyre::eyre;
use server::Uuid;
use frost::{
keys::PublicKeyPackage, round1::SigningCommitments, round2::SignatureShare, Identifier,
SigningPackage,
};
use std::{
collections::BTreeMap,
error::Error,
io::{BufRead, Write},
time::Duration,
};
use super::Comms;
use crate::args::Args;
pub struct HTTPComms {
client: reqwest::Client,
host_port: String,
session_id: Option<Uuid>,
}
impl HTTPComms {
pub fn new(args: &Args) -> Self {
let client = reqwest::Client::new();
Self {
client,
host_port: format!("http://{}:{}", args.ip, args.port),
session_id: None,
}
}
}
#[async_trait(?Send)]
impl Comms for HTTPComms {
async fn get_signing_commitments(
&mut self,
_input: &mut dyn BufRead,
_output: &mut dyn Write,
_pub_key_package: &PublicKeyPackage,
num_signers: u16,
) -> Result<BTreeMap<Identifier, SigningCommitments>, Box<dyn Error>> {
let r = self
.client
.post(format!("{}/create_new_session", self.host_port))
.json(&server::CreateNewSessionArgs {
num_signers,
message_count: 1,
})
.send()
.await?
.json::<server::CreateNewSessionOutput>()
.await?;
eprintln!(
"Send the following session ID to participants: {}",
r.session_id
);
self.session_id = Some(r.session_id);
eprint!("Waiting for participants to send their commitments...");
let r = loop {
let r = self
.client
.post(format!("{}/get_commitments", self.host_port))
.json(&server::GetCommitmentsArgs {
session_id: r.session_id,
})
.send()
.await?;
if r.status() != 200 {
tokio::time::sleep(Duration::from_secs(2)).await;
eprint!(".");
} else {
break r.json::<server::GetCommitmentsOutput>().await?;
}
};
eprintln!();
Ok(r.commitments
.first()
.ok_or(eyre!("empty commitments"))
.cloned()?)
}
async fn get_signature_shares(
&mut self,
_input: &mut dyn BufRead,
_output: &mut dyn Write,
signing_package: &SigningPackage,
#[cfg(feature = "redpallas")] randomizer: frost::round2::Randomizer,
) -> Result<BTreeMap<Identifier, SignatureShare>, Box<dyn Error>> {
// Send SigningPackage to all participants
eprintln!("Sending SigningPackage to participants...");
let _r = self
.client
.post(format!("{}/send_signing_package", self.host_port))
.json(&server::SendSigningPackageArgs {
aux_msg: Default::default(),
session_id: self.session_id.unwrap(),
signing_package: vec![signing_package.clone()],
#[cfg(feature = "redpallas")]
randomizer: vec![randomizer],
})
.send()
.await?
.bytes()
.await?;
eprintln!("Waiting for participants to send their SignatureShares...");
let r = loop {
let r = self
.client
.post(format!("{}/get_signature_shares", self.host_port))
.json(&server::GetSignatureSharesArgs {
session_id: self.session_id.unwrap(),
})
.send()
.await?;
if r.status() != 200 {
tokio::time::sleep(Duration::from_secs(2)).await;
eprint!(".");
} else {
break r.json::<server::GetSignatureSharesOutput>().await?;
}
};
eprintln!();
Ok(r.signature_shares
.first()
.ok_or(eyre!("empty signature shares"))?
.clone())
}
}

View File

@ -17,7 +17,12 @@ pub fn request_randomizer(
args: &Args,
input: &mut impl BufRead,
logger: &mut dyn Write,
signing_package: &SigningPackage,
) -> Result<frost::round2::Randomizer, Box<dyn std::error::Error>> {
if args.randomizer.is_empty() {
let rng = rand::thread_rng();
return Ok(frost::round2::Randomizer::new(rng, signing_package)?);
};
let randomizer = if args.randomizer == "-" {
writeln!(logger, "Enter the randomizer (hex string):")?;

View File

@ -10,6 +10,7 @@ axum = "0.7.3"
clap = { version = "4.5.2", features = ["derive"] }
derivative = "2.2.0"
eyre = "0.6.11"
frost-ed25519 = { version = "1.0.0-rc.0", features = ["serde"] }
rand = "0.8"
reddsa = { git = "https://github.com/ZcashFoundation/reddsa.git", rev = "81c649c412e5b6ba56d491d2857f91fbd28adbc7", features = [
"frost",
@ -24,5 +25,11 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "serde"] }
[dev-dependencies]
axum-test = "14.4.0"
reqwest = { version = "0.11.26", features = ["json"] }
axum-test = "14.2.2"
coordinator = { path = "../coordinator" }
reqwest = { version = "0.11.26", features = ["json"] }
regex = "1.10.3"
[features]
redpallas = ["coordinator/redpallas"]
default = []

View File

@ -158,9 +158,14 @@ pub(crate) async fn send_signing_package(
match &mut session.state {
SessionState::CommitmentsReady { commitments } => {
if args.signing_package.len() != session.message_count as usize
|| args.randomizer.len() != session.message_count as usize
{
if args.signing_package.len() != session.message_count as usize {
return Err(AppError(
StatusCode::INTERNAL_SERVER_ERROR,
eyre!("wrong number of inputs"),
));
}
#[cfg(feature = "redpallas")]
if args.randomizer.len() != session.message_count as usize {
return Err(AppError(
StatusCode::INTERNAL_SERVER_ERROR,
eyre!("wrong number of inputs"),
@ -170,6 +175,7 @@ pub(crate) async fn send_signing_package(
identifiers: commitments.keys().cloned().collect(),
signing_package: args.signing_package,
signature_shares: Default::default(),
#[cfg(feature = "redpallas")]
randomizer: args.randomizer,
aux_msg: args.aux_msg,
};
@ -202,10 +208,12 @@ pub(crate) async fn get_signing_package(
identifiers: _,
signing_package,
signature_shares: _,
#[cfg(feature = "redpallas")]
randomizer,
aux_msg,
} => Ok(Json(GetSigningPackageOutput {
signing_package: signing_package.clone(),
#[cfg(feature = "redpallas")]
randomizer: randomizer.clone(),
aux_msg: aux_msg.clone(),
})),
@ -238,7 +246,8 @@ pub(crate) async fn send_signature_share(
identifiers,
signing_package: _,
signature_shares,
randomizer: _,
#[cfg(feature = "redpallas")]
randomizer: _,
aux_msg: _,
} => {
if !identifiers.contains(&args.identifier) {

View File

@ -3,10 +3,13 @@ use std::{
sync::{Arc, RwLock},
};
use uuid::Uuid;
#[cfg(not(feature = "redpallas"))]
use frost_ed25519 as frost;
#[cfg(feature = "redpallas")]
use reddsa::frost::redpallas as frost;
use uuid::Uuid;
/// The current state of the server, and the required data for the state.
#[derive(derivative::Derivative)]
#[derivative(Debug)]
@ -35,7 +38,8 @@ pub enum SessionState {
/// Randomizer sent by coordinator to be sent to participants, for each
/// message being signed.
/// (Rerandomized FROST only. TODO: make it optional?)
#[derivative(Debug = "ignore")]
#[cfg(feature = "redpallas")]
#[cfg_attr(feature = "redpallas", derivative(Debug = "ignore"))]
randomizer: Vec<frost::round2::Randomizer>,
/// Auxiliary (optional) message. A context-specific data that is
/// supposed to be interpreted by the participants.

View File

@ -1,8 +1,11 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use uuid::Uuid;
#[cfg(not(feature = "redpallas"))]
use frost_ed25519 as frost;
#[cfg(feature = "redpallas")]
use reddsa::frost::redpallas as frost;
#[derive(Debug, Serialize, Deserialize)]
@ -50,7 +53,8 @@ pub struct SendSigningPackageArgs {
pub session_id: Uuid,
pub signing_package: Vec<frost::SigningPackage>,
pub aux_msg: Vec<u8>,
#[derivative(Debug = "ignore")]
#[cfg(feature = "redpallas")]
#[cfg_attr(feature = "redpallas", derivative(Debug = "ignore"))]
pub randomizer: Vec<frost::round2::Randomizer>,
}
@ -63,7 +67,8 @@ pub struct GetSigningPackageArgs {
#[derivative(Debug)]
pub struct GetSigningPackageOutput {
pub signing_package: Vec<frost::SigningPackage>,
#[derivative(Debug = "ignore")]
#[cfg(feature = "redpallas")]
#[cfg_attr(feature = "redpallas", derivative(Debug = "ignore"))]
pub randomizer: Vec<frost::round2::Randomizer>,
pub aux_msg: Vec<u8>,
}

View File

@ -4,6 +4,9 @@ use axum_test::TestServer;
use rand::thread_rng;
use server::{args::Args, router};
#[cfg(not(feature = "redpallas"))]
use frost_ed25519 as frost;
#[cfg(feature = "redpallas")]
use reddsa::frost::redpallas as frost;
/// Test the entire FROST signing flow using axum_test.
@ -110,6 +113,7 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
.enumerate()
.map(|(i, msg)| frost::SigningPackage::new(commitments[i].clone(), msg))
.collect::<Vec<_>>();
#[cfg(feature = "redpallas")]
let randomized_params = signing_packages
.iter()
.map(|p| frost::RandomizedParams::new(pubkeys.verifying_key(), p, &mut rng))
@ -121,6 +125,7 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
.json(&server::SendSigningPackageArgs {
session_id,
signing_package: signing_packages.clone(),
#[cfg(feature = "redpallas")]
randomizer: randomized_params.iter().map(|p| *p.randomizer()).collect(),
aux_msg: aux_msg.to_owned(),
})
@ -141,6 +146,7 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
let r: server::GetSigningPackageOutput = res.json();
// Generate SignatureShares for each SigningPackage
#[cfg(feature = "redpallas")]
let signature_share = r
.signing_package
.iter()
@ -156,6 +162,16 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
})
.collect::<Result<Vec<_>, _>>()?;
#[cfg(not(feature = "redpallas"))]
let signature_share = r
.signing_package
.iter()
.enumerate()
.map(|(i, signing_package)| {
frost::round2::sign(signing_package, &nonces_map[identifier][i], key_package)
})
.collect::<Result<Vec<_>, _>>()?;
// Send SignatureShares to the server
let res = server
.post("/send_signature_share")
@ -177,11 +193,18 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
let r: server::GetSignatureSharesOutput = res.json();
// Generate the final Signature for each message
#[cfg(feature = "redpallas")]
let signatures = signing_packages
.iter()
.enumerate()
.map(|(i, p)| frost::aggregate(p, &r.signature_shares[i], &pubkeys, &randomized_params[i]))
.collect::<Result<Vec<_>, _>>()?;
#[cfg(not(feature = "redpallas"))]
let signatures = signing_packages
.iter()
.enumerate()
.map(|(i, p)| frost::aggregate(p, &r.signature_shares[i], &pubkeys))
.collect::<Result<Vec<_>, _>>()?;
// Close the session
let res = server
@ -191,10 +214,15 @@ async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
res.assert_status_ok();
// Verify signatures to test if they were generated correctly
#[cfg(feature = "redpallas")]
for (i, p) in randomized_params.iter().enumerate() {
p.randomized_verifying_key()
.verify(messages[i], &signatures[i])?;
}
#[cfg(not(feature = "redpallas"))]
for (i, m) in messages.iter().enumerate() {
pubkeys.verifying_key().verify(m, &signatures[i])?;
}
Ok(())
}