Phase 2: add Server (#124)
* ongoing work * partial server implementation * preliminary version of all functions * feature complete, test working * improve documentation and tests
This commit is contained in:
parent
0091b9c24f
commit
0fe11c9b22
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -3,10 +3,16 @@ members = [
|
|||
"participant",
|
||||
"trusted-dealer",
|
||||
"dkg",
|
||||
"coordinator", "tests"
|
||||
"coordinator",
|
||||
"tests",
|
||||
"server",
|
||||
]
|
||||
default-members = ["participant",
|
||||
default-members = [
|
||||
"participant",
|
||||
"trusted-dealer",
|
||||
"dkg",
|
||||
"coordinator", "tests"]
|
||||
"coordinator",
|
||||
"tests",
|
||||
"server"
|
||||
]
|
||||
resolver = "2"
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.3"
|
||||
clap = { version = "4.4.14", features = ["derive"] }
|
||||
eyre = "0.6.11"
|
||||
rand = "0.8"
|
||||
reddsa = { git = "https://github.com/ZcashFoundation/reddsa.git", rev = "81c649c412e5b6ba56d491d2857f91fbd28adbc7", features = [
|
||||
"frost",
|
||||
"serde",
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.68"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "14.2.2"
|
||||
reqwest = { version = "0.11.23", features = ["json"] }
|
|
@ -0,0 +1,35 @@
|
|||
# FROST Server
|
||||
|
||||
|
||||
This is a HTTP server that allow clients (Coordinator and Participants) to
|
||||
run FROST without needing to directly connect to one another.
|
||||
|
||||
|
||||
## Status ⚠
|
||||
|
||||
This is a prototype which is NOT SECURE since messages are not encrypted nor
|
||||
authenticated. DO NOT USE this for anything other than testing.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
NOTE: This is for demo purposes only and should not be used in production.
|
||||
|
||||
You will need to have [Rust and Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) installed.
|
||||
|
||||
To run:
|
||||
1. Clone the repo. Run `git clone https://github.com/ZcashFoundation/frost-zcash-demo.git`
|
||||
2. Run `cargo install`
|
||||
3. Run `cargo run --bin server`
|
||||
|
||||
You can specify the IP and port to bind to using `--ip` and `--port`, e.g.
|
||||
`cargo run --bin server -- --ip 127.0.0.1 --port 2744`.
|
||||
|
||||
## TODO
|
||||
|
||||
- Add specific error codes
|
||||
- Remove frost-specific types (when data is encrypted)
|
||||
- Session timeouts
|
||||
- Encryption/authentication
|
||||
- DoS protections and other production-ready requirements
|
||||
-
|
|
@ -0,0 +1,13 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// IP to bind to
|
||||
#[arg(short, long, default_value = "0.0.0.0")]
|
||||
pub ip: String,
|
||||
|
||||
/// Port to bind to
|
||||
#[arg(short, long, default_value_t = 2744)]
|
||||
pub port: u16,
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use axum::{extract::State, http::StatusCode, Json};
|
||||
|
||||
use eyre::eyre;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
state::{Session, SessionState, SharedState},
|
||||
types::*,
|
||||
AppError,
|
||||
};
|
||||
|
||||
/// Implement the create_new_session API.
|
||||
pub(crate) async fn create_new_session(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<CreateNewSessionArgs>,
|
||||
) -> Result<Json<CreateNewSessionOutput>, AppError> {
|
||||
// Create new session object.
|
||||
let id = Uuid::new_v4();
|
||||
let session = Session {
|
||||
identifiers: args.identifiers.iter().cloned().collect(),
|
||||
state: SessionState::WaitingForCommitments {
|
||||
commitments: Default::default(),
|
||||
},
|
||||
};
|
||||
// Save session into global state.
|
||||
state.write().unwrap().sessions.insert(id, session);
|
||||
let user = CreateNewSessionOutput { session_id: id };
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
/// Implement the send_commitments API
|
||||
// TODO: get identifier from channel rather from arguments
|
||||
pub(crate) async fn send_commitments(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<SendCommitmentsArgs>,
|
||||
) -> Result<(), AppError> {
|
||||
// Get the mutex lock to read and write from the state
|
||||
let mut state_lock = state.write().unwrap();
|
||||
|
||||
let session = state_lock
|
||||
.sessions
|
||||
.get_mut(&args.session_id)
|
||||
.ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &mut session.state {
|
||||
SessionState::WaitingForCommitments { commitments } => {
|
||||
if !session.identifiers.contains(&args.identifier) {
|
||||
return Err(AppError(StatusCode::NOT_FOUND, eyre!("invalid identifier")));
|
||||
}
|
||||
// Add commitment to map.
|
||||
// Currently ignores the possibility of overwriting previous values
|
||||
// (it seems better to ignore overwrites, which could be caused by
|
||||
// poor networking connectivity leading to retries)
|
||||
commitments.insert(args.identifier, args.commitments);
|
||||
// If complete, advance to next state
|
||||
if commitments.keys().cloned().collect::<BTreeSet<_>>() == session.identifiers {
|
||||
session.state = SessionState::CommitmentsReady {
|
||||
commitments: commitments.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implement the get_commitments API
|
||||
pub(crate) async fn get_commitments(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<GetCommitmentsArgs>,
|
||||
) -> Result<Json<GetCommitmentsOutput>, AppError> {
|
||||
let state_lock = state.read().unwrap();
|
||||
|
||||
let session = state_lock.sessions.get(&args.session_id).ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &session.state {
|
||||
SessionState::CommitmentsReady { commitments } => Ok(Json(GetCommitmentsOutput {
|
||||
commitments: commitments.clone(),
|
||||
})),
|
||||
_ => Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the send_signing_package API
|
||||
pub(crate) async fn send_signing_package(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<SendSigningPackageArgs>,
|
||||
) -> Result<(), AppError> {
|
||||
let mut state_lock = state.write().unwrap();
|
||||
|
||||
let session = state_lock
|
||||
.sessions
|
||||
.get_mut(&args.session_id)
|
||||
.ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &mut session.state {
|
||||
SessionState::CommitmentsReady { .. } => {
|
||||
session.state = SessionState::WaitingForSignatureShares {
|
||||
signing_package: args.signing_package,
|
||||
signature_shares: Default::default(),
|
||||
randomizer: args.randomizer,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implement the get_signing_package API
|
||||
pub(crate) async fn get_signing_package(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<GetSigningPackageArgs>,
|
||||
) -> Result<Json<GetSigningPackageOutput>, AppError> {
|
||||
let state_lock = state.read().unwrap();
|
||||
|
||||
let session = state_lock.sessions.get(&args.session_id).ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &session.state {
|
||||
SessionState::WaitingForSignatureShares {
|
||||
signing_package,
|
||||
signature_shares: _,
|
||||
randomizer,
|
||||
} => Ok(Json(GetSigningPackageOutput {
|
||||
signing_package: signing_package.clone(),
|
||||
randomizer: *randomizer,
|
||||
})),
|
||||
_ => Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the send_signature_share API
|
||||
// TODO: get identifier from channel rather from arguments
|
||||
pub(crate) async fn send_signature_share(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<SendSignatureShareArgs>,
|
||||
) -> Result<(), AppError> {
|
||||
let mut state_lock = state.write().unwrap();
|
||||
|
||||
let session = state_lock
|
||||
.sessions
|
||||
.get_mut(&args.session_id)
|
||||
.ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &mut session.state {
|
||||
SessionState::WaitingForSignatureShares {
|
||||
signing_package: _,
|
||||
signature_shares,
|
||||
randomizer: _,
|
||||
} => {
|
||||
if !session.identifiers.contains(&args.identifier) {
|
||||
return Err(AppError(StatusCode::NOT_FOUND, eyre!("invalid identifier")));
|
||||
}
|
||||
// Currently ignoring the possibility of overwriting previous values
|
||||
// (it seems better to ignore overwrites, which could be caused by
|
||||
// poor networking connectivity leading to retries)
|
||||
signature_shares.insert(args.identifier, args.signature_share);
|
||||
// If complete, advance to next state
|
||||
if signature_shares.keys().cloned().collect::<BTreeSet<_>>() == session.identifiers {
|
||||
session.state = SessionState::SignatureSharesReady {
|
||||
signature_shares: signature_shares.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Implement the get_signature_shares API
|
||||
pub(crate) async fn get_signature_shares(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<GetSignatureSharesArgs>,
|
||||
) -> Result<Json<GetSignatureSharesOutput>, AppError> {
|
||||
let state_lock = state.read().unwrap();
|
||||
|
||||
let session = state_lock.sessions.get(&args.session_id).ok_or(AppError(
|
||||
StatusCode::NOT_FOUND,
|
||||
eyre!("session ID not found"),
|
||||
))?;
|
||||
|
||||
match &session.state {
|
||||
SessionState::SignatureSharesReady { signature_shares } => {
|
||||
Ok(Json(GetSignatureSharesOutput {
|
||||
signature_shares: signature_shares.clone(),
|
||||
}))
|
||||
}
|
||||
_ => Err(AppError(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
eyre!("incompatible session state"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the close_session API.
|
||||
pub(crate) async fn close_session(
|
||||
State(state): State<SharedState>,
|
||||
Json(args): Json<CloseSessionArgs>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
state.write().unwrap().sessions.remove(&args.session_id);
|
||||
Ok(Json(()))
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
pub mod args;
|
||||
mod functions;
|
||||
mod state;
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
use args::Args;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
|
||||
/// Create the axum Router for the server.
|
||||
/// Maps specific endpoints to handler functions.
|
||||
// TODO: use methods of a single object instead of separate functions?
|
||||
pub fn router() -> Router {
|
||||
// Shared state that is passed to each handler by axum
|
||||
let shared_state = state::SharedState::default();
|
||||
Router::new()
|
||||
.route("/create_new_session", post(functions::create_new_session))
|
||||
.route("/send_commitments", post(functions::send_commitments))
|
||||
.route("/get_commitments", post(functions::get_commitments))
|
||||
.route(
|
||||
"/send_signing_package",
|
||||
post(functions::send_signing_package),
|
||||
)
|
||||
.route("/get_signing_package", post(functions::get_signing_package))
|
||||
.route(
|
||||
"/send_signature_share",
|
||||
post(functions::send_signature_share),
|
||||
)
|
||||
.route(
|
||||
"/get_signature_shares",
|
||||
post(functions::get_signature_shares),
|
||||
)
|
||||
.route("/close_session", post(functions::close_session))
|
||||
.with_state(shared_state)
|
||||
}
|
||||
|
||||
/// Run the server with the specified arguments.
|
||||
pub async fn run(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app = router();
|
||||
|
||||
let addr = format!("{}:{}", args.ip, args.port);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
Ok(axum::serve(listener, app).await?)
|
||||
}
|
||||
|
||||
/// An error. Wraps a StatusCode which is returned by the server when the
|
||||
/// error happens during a API call, and a generic eyre::Report.
|
||||
// TODO: create an enum with specific errors
|
||||
pub struct AppError(StatusCode, eyre::Report);
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
(self.0, format!("{}", self.1)).into_response()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
use clap::Parser;
|
||||
use server::args::Args;
|
||||
use server::run;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
// initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
run(&args).await
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use reddsa::frost::redpallas as frost;
|
||||
|
||||
/// The current state of the server, and the required data for the state.
|
||||
pub enum SessionState {
|
||||
/// Waiting for participants to send their commitments.
|
||||
WaitingForCommitments {
|
||||
/// Commitments sent by participants so far.
|
||||
commitments: BTreeMap<frost::Identifier, frost::round1::SigningCommitments>,
|
||||
},
|
||||
/// Commitments have been sent by all participants; ready to be fetched by
|
||||
/// the coordinator. Waiting for coordinator to send the SigningPackage.
|
||||
CommitmentsReady {
|
||||
/// All commitments sent by participants.
|
||||
commitments: BTreeMap<frost::Identifier, frost::round1::SigningCommitments>,
|
||||
},
|
||||
/// SigningPackage ready to be fetched by participants. Waiting for
|
||||
/// participants to send their signature shares.
|
||||
WaitingForSignatureShares {
|
||||
/// SigningPackage sent by the coordinator to be sent to participants.
|
||||
signing_package: frost::SigningPackage,
|
||||
/// Randomizer sent by coordinator to be sent to participants
|
||||
/// (Rerandomized FROST only. TODO: make it optional?)
|
||||
randomizer: frost::round2::Randomizer,
|
||||
/// Signature shares sent by participants so far.
|
||||
signature_shares: BTreeMap<frost::Identifier, frost::round2::SignatureShare>,
|
||||
},
|
||||
/// SignatureShares have been sent by all participants; ready to be fetched
|
||||
/// by the coordinator.
|
||||
SignatureSharesReady {
|
||||
signature_shares: BTreeMap<frost::Identifier, frost::round2::SignatureShare>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for SessionState {
|
||||
fn default() -> Self {
|
||||
SessionState::WaitingForCommitments {
|
||||
commitments: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A particular signing session.
|
||||
pub struct Session {
|
||||
/// The set of identifiers for the session.
|
||||
pub(crate) identifiers: BTreeSet<frost::Identifier>,
|
||||
/// The session state.
|
||||
pub(crate) state: SessionState,
|
||||
}
|
||||
|
||||
/// The global state of the server.
|
||||
#[derive(Default)]
|
||||
pub struct AppState {
|
||||
/// Mapping of signing sessions by UUID.
|
||||
pub(crate) sessions: HashMap<Uuid, Session>,
|
||||
}
|
||||
|
||||
/// Type alias for the global state under a reference-counted RW mutex,
|
||||
/// which allows reading and writing the state across different handlers.
|
||||
pub type SharedState = Arc<RwLock<AppState>>;
|
|
@ -0,0 +1,73 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use reddsa::frost::redpallas as frost;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateNewSessionArgs {
|
||||
pub identifiers: Vec<frost::Identifier>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateNewSessionOutput {
|
||||
pub session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SendCommitmentsArgs {
|
||||
pub session_id: Uuid,
|
||||
pub identifier: frost::Identifier,
|
||||
pub commitments: frost::round1::SigningCommitments,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetCommitmentsArgs {
|
||||
pub session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetCommitmentsOutput {
|
||||
pub commitments: BTreeMap<frost::Identifier, frost::round1::SigningCommitments>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SendSigningPackageArgs {
|
||||
pub session_id: Uuid,
|
||||
pub signing_package: frost::SigningPackage,
|
||||
pub randomizer: frost::round2::Randomizer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSigningPackageArgs {
|
||||
pub session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSigningPackageOutput {
|
||||
pub signing_package: frost::SigningPackage,
|
||||
pub randomizer: frost::round2::Randomizer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SendSignatureShareArgs {
|
||||
pub session_id: Uuid,
|
||||
pub identifier: frost::Identifier,
|
||||
pub signature_share: frost::round2::SignatureShare,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSignatureSharesArgs {
|
||||
pub session_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GetSignatureSharesOutput {
|
||||
pub signature_shares: BTreeMap<frost::Identifier, frost::round2::SignatureShare>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CloseSessionArgs {
|
||||
pub session_id: Uuid,
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use axum_test::TestServer;
|
||||
use rand::thread_rng;
|
||||
use server::{args::Args, router};
|
||||
|
||||
use reddsa::frost::redpallas as frost;
|
||||
|
||||
/// Test the entire FROST signing flow using axum_test.
|
||||
/// This is a good example of the overall flow but it's not a good example
|
||||
/// of the client code, see the next test for that.
|
||||
///
|
||||
/// Also note that this simulates multiple clients using loops. In practice,
|
||||
/// each client will run independently.
|
||||
#[tokio::test]
|
||||
async fn test_main_router() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut rng = thread_rng();
|
||||
let (shares, pubkeys) =
|
||||
frost::keys::generate_with_dealer(3, 2, frost::keys::IdentifierList::Default, &mut rng)
|
||||
.unwrap();
|
||||
let key_packages: BTreeMap<_, _> = shares
|
||||
.iter()
|
||||
.map(|(identifier, secret_share)| {
|
||||
(
|
||||
*identifier,
|
||||
frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let router = router();
|
||||
let server = TestServer::new(router)?;
|
||||
let res = server
|
||||
.post("/create_new_session")
|
||||
.json(&server::CreateNewSessionArgs {
|
||||
identifiers: key_packages.keys().copied().collect::<Vec<_>>(),
|
||||
})
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let r: server::CreateNewSessionOutput = res.json();
|
||||
let session_id = r.session_id;
|
||||
|
||||
let mut nonces_map = BTreeMap::<_, _>::new();
|
||||
for (identifier, key_package) in key_packages.iter() {
|
||||
let (nonces, commitments) = frost::round1::commit(key_package.signing_share(), &mut rng);
|
||||
nonces_map.insert(*identifier, nonces);
|
||||
let res = server
|
||||
.post("/send_commitments")
|
||||
.json(&server::SendCommitmentsArgs {
|
||||
identifier: *identifier,
|
||||
session_id,
|
||||
commitments,
|
||||
})
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
}
|
||||
|
||||
let res = server
|
||||
.post("/get_commitments")
|
||||
.json(&server::GetCommitmentsArgs { session_id })
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let r: server::GetCommitmentsOutput = res.json();
|
||||
let commitments = r.commitments;
|
||||
|
||||
let message = "Hello, world!".as_bytes();
|
||||
let signing_package = frost::SigningPackage::new(commitments, message);
|
||||
let randomized_params =
|
||||
frost::RandomizedParams::new(pubkeys.verifying_key(), &signing_package, &mut rng)?;
|
||||
|
||||
let res = server
|
||||
.post("/send_signing_package")
|
||||
.json(&server::SendSigningPackageArgs {
|
||||
session_id,
|
||||
signing_package: signing_package.clone(),
|
||||
randomizer: *randomized_params.randomizer(),
|
||||
})
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
|
||||
for (identifier, key_package) in key_packages.iter() {
|
||||
let res = server
|
||||
.post("get_signing_package")
|
||||
.json(&server::GetSigningPackageArgs { session_id })
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let r: server::GetSigningPackageOutput = res.json();
|
||||
|
||||
let signature_share = frost::round2::sign(
|
||||
&r.signing_package,
|
||||
&nonces_map[identifier],
|
||||
key_package,
|
||||
r.randomizer,
|
||||
)?;
|
||||
|
||||
let res = server
|
||||
.post("/send_signature_share")
|
||||
.json(&server::SendSignatureShareArgs {
|
||||
session_id,
|
||||
identifier: *identifier,
|
||||
signature_share,
|
||||
})
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
}
|
||||
|
||||
let res = server
|
||||
.post("/get_signature_shares")
|
||||
.json(&server::GetSignatureSharesArgs { session_id })
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let r: server::GetSignatureSharesOutput = res.json();
|
||||
|
||||
let signature = frost::aggregate(
|
||||
&signing_package,
|
||||
&r.signature_shares,
|
||||
&pubkeys,
|
||||
&randomized_params,
|
||||
)?;
|
||||
|
||||
randomized_params
|
||||
.randomized_verifying_key()
|
||||
.verify(message, &signature)?;
|
||||
|
||||
let res = server
|
||||
.post("/close_session")
|
||||
.json(&server::CloseSessionArgs { session_id })
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
println!("{}", res.text());
|
||||
let _: () = res.json();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Actually spawn the HTTP server and connect to it using reqwest.
|
||||
/// A better example on how to write client code.
|
||||
#[tokio::test]
|
||||
async fn test_http() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create test values
|
||||
let mut rng = thread_rng();
|
||||
let (shares, _pubkeys) =
|
||||
frost::keys::generate_with_dealer(3, 2, frost::keys::IdentifierList::Default, &mut rng)
|
||||
.unwrap();
|
||||
let key_packages: BTreeMap<_, _> = shares
|
||||
.iter()
|
||||
.map(|(identifier, secret_share)| {
|
||||
(
|
||||
*identifier,
|
||||
frost::keys::KeyPackage::try_from(secret_share.clone()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Spawn server for testing
|
||||
tokio::spawn(async move {
|
||||
server::run(&Args {
|
||||
ip: "127.0.0.1".to_string(),
|
||||
port: 2744,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Wait for server to start listening
|
||||
// TODO: this could possibly be not enough, use some retry logic instead
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Call create_new_session
|
||||
let client = reqwest::Client::new();
|
||||
let r = client
|
||||
.post("http://127.0.0.1:2744/create_new_session")
|
||||
.json(&server::CreateNewSessionArgs {
|
||||
identifiers: key_packages.keys().copied().collect::<Vec<_>>(),
|
||||
})
|
||||
.send()
|
||||
.await?
|
||||
.json::<server::CreateNewSessionOutput>()
|
||||
.await?;
|
||||
println!("{}", r.session_id);
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue