diff --git a/Cargo.lock b/Cargo.lock index 6d96c0f71..4e3ba2f4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,15 +110,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "autotools" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8da1805e028a172334c3b680f93e71126f2327622faef2ec3d893c0a4ad77" -dependencies = [ - "cc", -] - [[package]] name = "axum" version = "0.6.12" @@ -201,15 +192,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bollard" version = "0.14.0" @@ -365,31 +347,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" -[[package]] -name = "cpufeatures" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" -dependencies = [ - "libc", -] - [[package]] name = "crunchy" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "cxx" version = "1.0.94" @@ -434,35 +397,6 @@ dependencies = [ "syn 2.0.12", ] -[[package]] -name = "dashmap" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" -dependencies = [ - "cfg-if", - "hashbrown", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - [[package]] name = "errno" version = "0.3.0" @@ -506,15 +440,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "ff" version = "0.6.0" @@ -540,12 +465,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "fnv" version = "1.0.7" @@ -650,16 +569,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.1.16" @@ -873,15 +782,6 @@ dependencies = [ "serde", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-lifetimes" version = "1.0.9" @@ -905,15 +805,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.6" @@ -1034,8 +925,7 @@ dependencies = [ "clap", "futures", "hex", - "ractor", - "ractor_cluster", + "hyper", "rand 0.7.3", "serde", "serde_json", @@ -1062,12 +952,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1169,7 +1053,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", "windows-sys", ] @@ -1180,16 +1064,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" -[[package]] -name = "petgraph" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" -dependencies = [ - "fixedbitset", - "indexmap", -] - [[package]] name = "pin-project" version = "1.0.12" @@ -1237,16 +1111,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "prettyplease" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" -dependencies = [ - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "proc-macro2" version = "1.0.54" @@ -1256,69 +1120,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48e50df39172a3e7eb17e14642445da64996989bc212b583015435d39a58537" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c828f93f5ca4826f97fedcbd3f9a536c16b12cff3dbbb4a007f932bbad95b12" -dependencies = [ - "bytes", - "heck", - "itertools", - "lazy_static", - "log", - "multimap", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 1.0.109", - "tempfile", - "which", -] - -[[package]] -name = "prost-derive" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379119666929a1afd7a043aa6cf96fa67a6dce9af60c88095a4686dbce4c9c88" -dependencies = [ - "prost", -] - -[[package]] -name = "protobuf-src" -version = "1.1.0+21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ac8852baeb3cc6fb83b93646fb93c0ffe5d14bf138c945ceb4b9948ee0e3c1" -dependencies = [ - "autotools", -] - [[package]] name = "quote" version = "1.0.26" @@ -1328,53 +1129,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "ractor" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa63565db47d63472058101694a12cf169d2aa2f63d243fdb3876c1e0d7cc31" -dependencies = [ - "async-trait", - "dashmap", - "futures", - "log", - "once_cell", - "rand 0.8.5", - "tokio", -] - -[[package]] -name = "ractor_cluster" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec2464b65ce7e8b4d6c1bf37d24b0df45d3d1be2f33146fbb23fa8cba877859" -dependencies = [ - "async-trait", - "bytes", - "log", - "prost", - "prost-build", - "prost-types", - "protobuf-src", - "ractor", - "ractor_cluster_derive", - "rand 0.8.5", - "rustls", - "sha2", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "ractor_cluster_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f06fdb22d26f2f0b8621ee96cbd2a18e4598a373abda614da337c475c963566" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "rand" version = "0.7.3" @@ -1464,15 +1218,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.7.3" @@ -1497,21 +1242,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "rustc-demangle" version = "0.1.22" @@ -1532,18 +1262,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "rustls" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - [[package]] name = "rustversion" version = "1.0.12" @@ -1568,16 +1286,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "serde" version = "1.0.159" @@ -1656,17 +1364,6 @@ dependencies = [ "time", ] -[[package]] -name = "sha2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.4" @@ -1710,12 +1407,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "strsim" version = "0.10.0" @@ -1762,19 +1453,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tempfile" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys", -] - [[package]] name = "termcolor" version = "1.2.0" @@ -1915,17 +1593,6 @@ dependencies = [ "syn 2.0.12", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls", - "tokio", - "webpki", -] - [[package]] name = "tokio-util" version = "0.7.7" @@ -2037,12 +1704,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2076,12 +1737,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "url" version = "2.3.1" @@ -2105,12 +1760,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - [[package]] name = "want" version = "0.3.0" @@ -2187,37 +1836,6 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs index e1d7b0c4f..d388c5689 100644 --- a/integration-tests/tests/lib.rs +++ b/integration-tests/tests/lib.rs @@ -3,13 +3,13 @@ use bollard::network::CreateNetworkOptions; use bollard::service::{HostConfig, Ipam, PortBinding}; use bollard::Docker; use futures::StreamExt; -use hyper::{body::HttpBody, Body, Client, Method, Request}; +use hyper::{Body, Client, Method, Request}; +use mpc_recovery::msg::LeaderResponse; use rand::{distributions::Alphanumeric, Rng}; use serde_json::json; use std::collections::HashMap; -use std::convert::TryInto; use std::time::Duration; -use threshold_crypto::{serde_impl::SerdeSecret, PublicKeySet, SecretKeyShare, Signature}; +use threshold_crypto::{serde_impl::SerdeSecret, PublicKeySet, SecretKeyShare}; use tokio::io::AsyncWriteExt; use tokio::spawn; @@ -45,44 +45,22 @@ async fn continuously_print_docker_output(docker: &Docker, id: &str) -> anyhow:: async fn start_mpc_node( docker: &Docker, - node_id: u64, - pk_set: &PublicKeySet, - sk_share: &SecretKeyShare, - actor_address: Option, -) -> anyhow::Result<(String, String, String)> { - let actor_port = portpicker::pick_unused_port().expect("no free ports"); - let web_port = portpicker::pick_unused_port().expect("no free ports"); - - let empty = HashMap::<(), ()>::new(); + cmd: Vec, + web_port: u16, + expose_web_port: bool, +) -> anyhow::Result<(String, String)> { let mut exposed_ports = HashMap::new(); - exposed_ports.insert(format!("{actor_port}/tcp"), empty.clone()); - exposed_ports.insert(format!("{web_port}/tcp"), empty); let mut port_bindings = HashMap::new(); - port_bindings.insert( - format!("{actor_port}/tcp"), - Some(vec![PortBinding { - host_ip: None, - host_port: Some(actor_port.to_string()), - }]), - ); - port_bindings.insert( - format!("{web_port}/tcp"), - Some(vec![PortBinding { - host_ip: None, - host_port: Some(web_port.to_string()), - }]), - ); - - let mut cmd = vec![ - "start".to_string(), - node_id.to_string(), - serde_json::to_string(&pk_set)?, - serde_json::to_string(&SerdeSecret(sk_share))?, - actor_port.to_string(), - web_port.to_string(), - ]; - if let Some(actor_address) = actor_address { - cmd.push(actor_address); + if expose_web_port { + let empty = HashMap::<(), ()>::new(); + exposed_ports.insert(format!("{web_port}/tcp"), empty); + port_bindings.insert( + format!("{web_port}/tcp"), + Some(vec![PortBinding { + host_ip: None, + host_port: Some(web_port.to_string()), + }]), + ); } let mpc_recovery_config = Config { @@ -123,11 +101,60 @@ async fn start_mpc_node( .ip_address .unwrap(); - Ok(( - id, - format!("{ip_address}:{actor_port}"), - format!("localhost:{web_port}"), - )) + Ok((id, ip_address)) +} + +async fn start_mpc_leader_node( + docker: &Docker, + node_id: u64, + pk_set: &PublicKeySet, + sk_share: &SecretKeyShare, + sign_nodes: Vec, +) -> anyhow::Result { + let web_port = portpicker::pick_unused_port().expect("no free ports"); + + let mut cmd = vec![ + "start-leader".to_string(), + node_id.to_string(), + "--pk-set".to_string(), + serde_json::to_string(&pk_set)?, + "--sk-share".to_string(), + serde_json::to_string(&SerdeSecret(sk_share))?, + "--web-port".to_string(), + web_port.to_string(), + ]; + for sign_node in sign_nodes { + cmd.push("--sign-nodes".to_string()); + cmd.push(sign_node); + } + + start_mpc_node(docker, cmd, web_port, true).await?; + // exposed host address + Ok(format!("http://localhost:{web_port}")) +} + +async fn start_mpc_sign_node( + docker: &Docker, + node_id: u64, + pk_set: &PublicKeySet, + sk_share: &SecretKeyShare, +) -> anyhow::Result { + let web_port = portpicker::pick_unused_port().expect("no free ports"); + + let cmd = vec![ + "start-sign".to_string(), + node_id.to_string(), + "--pk-set".to_string(), + serde_json::to_string(&pk_set)?, + "--sk-share".to_string(), + serde_json::to_string(&SerdeSecret(sk_share))?, + "--web-port".to_string(), + web_port.to_string(), + ]; + + let (_, ip_address) = start_mpc_node(docker, cmd, web_port, false).await?; + // internal network address + Ok(format!("http://{ip_address}:{web_port}")) } async fn create_network(docker: &Docker) -> anyhow::Result<()> { @@ -167,27 +194,16 @@ async fn test_trio() -> anyhow::Result<()> { // but only instantiates 3 nodes. let (pk_set, sk_shares) = mpc_recovery::generate(4, 3)?; - let mut actor_addresses = Vec::new(); - let mut web_ports = Vec::new(); - for (i, sk_share) in sk_shares.into_iter().enumerate().take(3) { - let (_id, actor_address, web_port) = start_mpc_node( - &docker, - (i + 1) as u64, - &pk_set, - &sk_share, - actor_addresses.first().cloned(), - ) - .await?; - actor_addresses.push(actor_address); - web_ports.push(web_port); + let mut sign_nodes = Vec::new(); + for i in 2..=3 { + let addr = start_mpc_sign_node(&docker, i as u64, &pk_set, &sk_shares[i - 1]).await?; + sign_nodes.push(addr); } + let leader_node = start_mpc_leader_node(&docker, 1, &pk_set, &sk_shares[0], sign_nodes).await?; // Wait until all nodes initialize tokio::time::sleep(Duration::from_millis(2000)).await; - // TODO: only leader node works for now, other nodes struggling to connect to each other - // for some reason. - let web_port = &web_ports[0]; let payload: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(10) @@ -195,28 +211,22 @@ async fn test_trio() -> anyhow::Result<()> { .collect(); let req = Request::builder() .method(Method::POST) - .uri(format!("http://{}/submit", web_port)) + .uri(format!("{}/submit", leader_node)) .header("content-type", "application/json") .body(Body::from(json!({ "payload": payload }).to_string()))?; let client = Client::new(); - let mut resp = client.request(req).await?; + let response = client.request(req).await?; - assert_eq!(resp.status(), 200); + assert_eq!(response.status(), 200); - let data = resp.body_mut().data().await.expect("no response body")?; - let response_body: String = serde_json::from_slice(&data)?; - let signature_bytes = hex::decode(response_body)?; - let signature_array: [u8; 96] = signature_bytes.as_slice().try_into().map_err(|_e| { - anyhow::anyhow!( - "signature has incorrect length: expected 96 bytes, but got {}", - signature_bytes.len() - ) - })?; - let signature = Signature::from_bytes(signature_array) - .map_err(|e| anyhow::anyhow!("malformed signature: {}", e))?; - - assert!(pk_set.public_key().verify(&signature, payload)); + let data = hyper::body::to_bytes(response).await?; + let response: LeaderResponse = serde_json::from_slice(&data)?; + if let LeaderResponse::Ok { signature } = response { + assert!(pk_set.public_key().verify(&signature, payload)); + } else { + panic!("response was not successful"); + } Ok(()) } diff --git a/mpc-recovery/Cargo.toml b/mpc-recovery/Cargo.toml index a77b20a23..7c379309e 100644 --- a/mpc-recovery/Cargo.toml +++ b/mpc-recovery/Cargo.toml @@ -23,8 +23,7 @@ axum = "0.6" clap = { version = "4.2", features = ["derive"] } futures = "0.3" hex = "0.4" -ractor = { version = "0.7", features = ["cluster"] } -ractor_cluster = "0.7" +hyper = { version = "0.14", features = ["full"] } actix-rt = "2.8" threshold_crypto = "0.4.0" rand = "0.7" diff --git a/mpc-recovery/src/actor.rs b/mpc-recovery/src/actor.rs deleted file mode 100644 index ed367baf0..000000000 --- a/mpc-recovery/src/actor.rs +++ /dev/null @@ -1,219 +0,0 @@ -use futures::prelude::*; -use futures::stream::FuturesUnordered; -use ractor::{ - concurrency::Duration, Actor, ActorProcessingErr, ActorRef, BytesConvertable, RpcReplyPort, -}; -use ractor_cluster::RactorClusterMessage; -use serde::{Deserialize, Serialize}; -use threshold_crypto::{PublicKeySet, SecretKeyShare, Signature, SignatureShare}; - -use crate::NodeId; - -const MPC_RECOVERY_GROUP: &str = "mpc-recovery"; - -type Payload = Vec; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignResponse { - node_id: NodeId, - sig_share: SignatureShare, -} - -impl BytesConvertable for SignResponse { - fn into_bytes(self) -> Vec { - serde_json::to_vec(&self).unwrap() - } - - fn from_bytes(bytes: Vec) -> Self { - serde_json::from_slice(&bytes).unwrap() - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignatureResponse { - pub sig: Signature, -} - -impl BytesConvertable for SignatureResponse { - fn into_bytes(self) -> Vec { - serde_json::to_vec(&self).unwrap() - } - - fn from_bytes(bytes: Vec) -> Self { - serde_json::from_slice(&bytes).unwrap() - } -} - -#[derive(RactorClusterMessage, Debug)] -pub enum NodeMessage { - #[rpc] - NewRequest(Payload, RpcReplyPort), - #[rpc] - SignRequest(Payload, RpcReplyPort), -} - -pub struct NodeActor; - -pub struct NodeActorState { - id: NodeId, - pk_set: PublicKeySet, - sk_share: SecretKeyShare, -} - -#[async_trait::async_trait] -impl Actor for NodeActor { - type Msg = NodeMessage; - type State = NodeActorState; - type Arguments = (NodeId, PublicKeySet, SecretKeyShare); - - #[tracing::instrument(level = "debug", skip_all, fields(id = args.0))] - async fn pre_start( - &self, - myself: ActorRef, - args: (NodeId, PublicKeySet, SecretKeyShare), - ) -> Result { - tracing::debug!(group = MPC_RECOVERY_GROUP, "joining"); - ractor::pg::join(MPC_RECOVERY_GROUP.to_string(), vec![myself.get_cell()]); - // create the initial state - Ok(NodeActorState { - id: args.0, - pk_set: args.1, - sk_share: args.2, - }) - } - - #[tracing::instrument(level = "debug", skip_all, fields(id = state.id, message))] - async fn handle( - &self, - _myself: ActorRef, - message: Self::Msg, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - let remote_actors = ractor::pg::get_members(&MPC_RECOVERY_GROUP.to_string()) - .into_iter() - .filter(|actor| !actor.get_id().is_local()) - .map(ActorRef::::from) - .collect::>(); - tracing::debug!( - remote_actors = ?remote_actors.iter().map(|a| a.get_id()).collect::>(), - "connected to" - ); - - match message { - NodeMessage::NewRequest(payload, reply) => { - tracing::debug!(?payload, "new request"); - state - .handle_new_request(payload, reply, &remote_actors) - .await - } - NodeMessage::SignRequest(payload, reply) => { - tracing::debug!(?payload, "sign request"); - state.handle_signed_msg(payload, reply) - } - }; - Ok(()) - } -} - -impl NodeActorState { - fn sign(&self, payload: &[u8]) -> SignResponse { - SignResponse { - node_id: self.id, - sig_share: self.sk_share.sign(payload), - } - } - - #[tracing::instrument(level = "debug", skip_all)] - fn handle_signed_msg(&mut self, payload: Payload, reply: RpcReplyPort) { - // TODO: run some check that the msg.task.payload makes sense, fail if not - tracing::debug!("approved"); - - let response = self.sign(&payload); - tracing::debug!(?response, "replying"); - - match reply.send(response) { - Ok(()) => {} - Err(e) => tracing::error!("failed to respond: {}", e), - }; - } - - async fn handle_new_request( - &mut self, - payload: Payload, - reply: RpcReplyPort, - remote_actors: &Vec>, - ) { - // TODO: run some check that the payload makes sense, fail if not - tracing::debug!("approved"); - - let mut futures = Vec::new(); - for actor in remote_actors { - tracing::debug!(actor = ?actor.get_id(), "asking actor"); - let future = actor - .call( - |tx| NodeMessage::SignRequest(payload.clone(), tx), - Some(Duration::from_millis(2000)), - ) - .map(|r| r.map_err(ractor::RactorErr::from)) - .map(|r| match r { - Ok(ractor::rpc::CallResult::Success(ok_value)) => Ok(ok_value), - Ok(cr) => Err(ractor::RactorErr::from(cr)), - Err(e) => Err(e), - }); - futures.push(future); - } - - // create unordered collection of futures - let futures = futures.into_iter().collect::>(); - - let mut responses = futures - .collect::>() - .await - .into_iter() - .filter_map(|r| r.ok()) - .collect::>(); - - let response = self.sign(&payload); - tracing::debug!(?response, "adding response from self"); - responses.push(response); - - tracing::debug!( - ?responses, - "got {} successful responses total", - responses.len() - ); - - let mut sig_shares = Vec::new(); - for sign_response in &responses { - if self - .pk_set - .public_key_share(sign_response.node_id) - .verify(&sign_response.sig_share, &payload) - { - sig_shares.push((sign_response.node_id, &sign_response.sig_share)); - } else { - tracing::error!(?sign_response, "received invalid signature",); - } - } - - tracing::debug!( - ?sig_shares, - "got {} valid signature shares total", - sig_shares.len() - ); - - if let Ok(sig) = self - .pk_set - .combine_signatures(sig_shares.clone().into_iter()) - { - tracing::debug!(?sig, "replying with full signature"); - reply.send(SignatureResponse { sig }).unwrap(); - } else { - tracing::error!( - "expected to get at least {} shares, but only got {}", - self.pk_set.threshold() + 1, - sig_shares.len() - ); - } - } -} diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs new file mode 100644 index 000000000..380eaaa5a --- /dev/null +++ b/mpc-recovery/src/leader_node/mod.rs @@ -0,0 +1,164 @@ +use crate::msg::{LeaderRequest, LeaderResponse, SigShareRequest, SigShareResponse}; +use crate::NodeId; +use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +use futures::stream::FuturesUnordered; +use hyper::client::ResponseFuture; +use hyper::{Body, Client, Method, Request}; +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; +use std::net::SocketAddr; +use threshold_crypto::{PublicKeySet, SecretKeyShare}; + +#[tracing::instrument(level = "debug", skip(pk_set, sk_share, sign_nodes))] +pub async fn run( + id: NodeId, + pk_set: PublicKeySet, + sk_share: SecretKeyShare, + port: u16, + sign_nodes: Vec, +) { + tracing::debug!(?sign_nodes, "running a leader node"); + + if pk_set.public_key_share(id) != sk_share.public_key_share() { + tracing::error!("provided secret share does not match the node id"); + return; + } + + let state = LeaderState { + id, + pk_set, + sk_share, + sign_nodes, + }; + + let app = Router::new() + .route("/submit", post(submit)) + .with_state(state); + + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + tracing::debug!(?addr, "starting http server"); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[derive(Clone)] +struct LeaderState { + id: NodeId, + pk_set: PublicKeySet, + sk_share: SecretKeyShare, + sign_nodes: Vec, +} + +async fn parse(response_future: ResponseFuture) -> anyhow::Result { + let response = response_future.await?; + let response_body = hyper::body::to_bytes(response.into_body()).await?; + Ok(serde_json::from_slice(&response_body)?) +} + +#[tracing::instrument(level = "debug", skip_all, fields(id = state.id))] +async fn submit( + State(state): State, + Json(request): Json, +) -> (StatusCode, Json) { + tracing::info!(payload = request.payload, "submit request"); + + // TODO: run some check that the payload makes sense, fail if not + tracing::debug!("approved"); + + let sig_share_request = SigShareRequest { + payload: request.payload.clone(), + }; + let payload_json = match serde_json::to_string(&sig_share_request) { + Ok(payload_json) => payload_json, + Err(err) => { + tracing::error!(%err, "failed to convert payload back to json"); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(LeaderResponse::Err)); + } + }; + + let response_futures = FuturesUnordered::new(); + for sign_node in state.sign_nodes { + let req = match Request::builder() + .method(Method::POST) + .uri(format!("{}/sign", sign_node)) + .header("content-type", "application/json") + .body(Body::from(payload_json.clone())) + { + Ok(req) => req, + Err(err) => { + tracing::error!(%err, "failed to construct a compute request"); + continue; + } + }; + + let client = Client::new(); + response_futures.push(client.request(req)); + } + + let mut sig_shares = BTreeMap::new(); + sig_shares.insert(state.id, state.sk_share.sign(&request.payload)); + for response_future in response_futures { + let response = match parse(response_future).await { + Ok(response) => response, + Err(err) => { + tracing::error!(%err, "failed to get response"); + continue; + } + }; + + if state + .pk_set + .public_key_share(response.node_id) + .verify(&response.sig_share, &request.payload) + { + match sig_shares.entry(response.node_id) { + Entry::Vacant(e) => { + tracing::debug!(?response, "received valid signature share"); + e.insert(response.sig_share); + } + Entry::Occupied(e) if e.get() == &response.sig_share => { + tracing::error!( + node_id = response.node_id, + sig_share = ?e.get(), + "received a duplicate share" + ); + } + Entry::Occupied(e) => { + tracing::error!( + node_id = response.node_id, + sig_share_1 = ?e.get(), + sig_share_2 = ?response.sig_share, + "received two different valid shares for the same node (should be impossible)" + ); + } + } + } else { + tracing::error!(?response, "received invalid signature",); + } + + if sig_shares.len() > state.pk_set.threshold() { + tracing::debug!( + "received {} valid signature shares, not waiting for the rest", + sig_shares.len() + ); + break; + } + } + + let sig_shares_num = sig_shares.len(); + tracing::debug!("got {} valid signature shares", sig_shares_num); + + if let Ok(signature) = state.pk_set.combine_signatures(&sig_shares) { + tracing::debug!(?signature, "replying with full signature"); + (StatusCode::OK, Json(LeaderResponse::Ok { signature })) + } else { + tracing::error!( + "expected to get at least {} shares, but only got {}", + state.pk_set.threshold() + 1, + sig_shares_num + ); + (StatusCode::INTERNAL_SERVER_ERROR, Json(LeaderResponse::Err)) + } +} diff --git a/mpc-recovery/src/lib.rs b/mpc-recovery/src/lib.rs index dd146c516..855e9792b 100644 --- a/mpc-recovery/src/lib.rs +++ b/mpc-recovery/src/lib.rs @@ -1,99 +1,13 @@ -use actix_rt::task::JoinHandle; -use actor::NodeActor; -use ractor::{Actor, ActorRef}; -use ractor_cluster::{node::NodeConnectionMode, NodeServer}; use threshold_crypto::{PublicKeySet, SecretKeySet, SecretKeyShare}; -mod actor; -mod web; - -const COOKIE: &str = "mpc-recovery-cookie"; -// TODO: not sure if hostname matters in ractor, but localhost seems to be working fine even in Docker networks -const HOSTNAME: &str = "localhost"; +mod leader_node; +pub mod msg; +mod sign_node; type NodeId = u64; -async fn start_node_server( - node_id: u64, - node_port: u16, -) -> anyhow::Result<(ActorRef, JoinHandle<()>)> { - let node_name = format!("mpc-recovery-node-{}", node_id); - tracing::debug!( - node_port, - cookie = COOKIE, - node_name, - hostname = HOSTNAME, - "starting node server" - ); - - let server = ractor_cluster::NodeServer::new( - node_port, - COOKIE.to_string(), - node_name, - HOSTNAME.to_string(), - None, - Some(NodeConnectionMode::Transitive), - ); - Actor::spawn(None, server, ()) - .await - .map_err(|_e| anyhow::anyhow!("failed to start node server")) -} - -async fn start_actor( - node_id: u64, - pk_set: PublicKeySet, - sk_share: SecretKeyShare, -) -> anyhow::Result<(ActorRef, JoinHandle<()>)> { - // Printing shortened hash should be enough for most use cases, but if you enable TRACE level - // you can see the entire curve details. - if tracing::level_enabled!(tracing::Level::TRACE) { - tracing::trace!(?pk_set, "starting node actor"); - } else { - tracing::debug!(public_key = ?pk_set.public_key(), "starting node actor"); - } - Actor::spawn(None, actor::NodeActor, (node_id, pk_set.clone(), sk_share)) - .await - .map_err(|_e| anyhow::anyhow!("failed to start actor")) -} - -#[tracing::instrument(level = "debug", skip_all, fields(id = node_id))] -pub async fn start( - node_id: u64, - pk_set: PublicKeySet, - sk_share: SecretKeyShare, - node_port: u16, - web_port: u16, - remote_addr: Option, -) -> anyhow::Result<()> { - let (node_server, node_server_handle) = start_node_server(node_id, node_port).await?; - let (node_actor, node_actor_handle) = start_actor(node_id, pk_set, sk_share).await?; - - if let Some(raddress) = remote_addr { - if let Err(error) = ractor_cluster::node::client::connect(&node_server, &raddress).await { - anyhow::bail!("failed to connect with error {error}"); - } else { - tracing::info!(raddress, "connected to remote node server"); - } - } else { - tracing::info!("no remote node server address provided, treating this node as the leader"); - } - - // start a user-facing web server - web::serve(node_id, web_port, node_actor.clone()).await; - - // wait for exit - tokio::signal::ctrl_c() - .await - .expect("failed to listen for event"); - - // cleanup - node_actor.stop(None); - node_actor_handle.await.unwrap(); - node_server.stop(None); - node_server_handle.await.unwrap(); - - Ok(()) -} +pub use leader_node::run as run_leader_node; +pub use sign_node::run as run_sign_node; #[tracing::instrument(level = "debug", skip_all, fields(n = n, threshold = t))] pub fn generate(n: usize, t: usize) -> anyhow::Result<(PublicKeySet, Vec)> { diff --git a/mpc-recovery/src/main.rs b/mpc-recovery/src/main.rs index a296629fb..48d2fb9f9 100644 --- a/mpc-recovery/src/main.rs +++ b/mpc-recovery/src/main.rs @@ -7,19 +7,34 @@ enum Cli { n: usize, t: usize, }, - Start { + StartLeader { /// Node ID node_id: u64, /// Root public key + #[arg(long)] pk_set: String, /// Secret key share + #[arg(long)] sk_share: String, - /// The actor port for this server - actor_port: u16, /// The web port for this server + #[arg(long)] + web_port: u16, + /// The compute nodes to connect to + #[arg(long)] + sign_nodes: Vec, + }, + StartSign { + /// Node ID + node_id: u64, + /// Root public key + #[arg(long)] + pk_set: String, + /// Secret key share + #[arg(long)] + sk_share: String, + /// The web port for this server + #[arg(long)] web_port: u16, - /// The remote server address to connect to (if Some) - remote_address: Option, }, } @@ -41,26 +56,28 @@ async fn main() -> anyhow::Result<()> { ); } } - Cli::Start { + Cli::StartLeader { + node_id, + pk_set, + sk_share, + web_port, + sign_nodes, + } => { + let pk_set: PublicKeySet = serde_json::from_str(&pk_set).unwrap(); + let sk_share: SecretKeyShare = serde_json::from_str(&sk_share).unwrap(); + + mpc_recovery::run_leader_node(node_id, pk_set, sk_share, web_port, sign_nodes).await; + } + Cli::StartSign { node_id, pk_set, sk_share, - actor_port, web_port, - remote_address, } => { let pk_set: PublicKeySet = serde_json::from_str(&pk_set).unwrap(); let sk_share: SecretKeyShare = serde_json::from_str(&sk_share).unwrap(); - mpc_recovery::start( - node_id, - pk_set, - sk_share, - actor_port, - web_port, - remote_address, - ) - .await?; + mpc_recovery::run_sign_node(node_id, pk_set, sk_share, web_port).await; } } diff --git a/mpc-recovery/src/msg.rs b/mpc-recovery/src/msg.rs new file mode 100644 index 000000000..ecddbe419 --- /dev/null +++ b/mpc-recovery/src/msg.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; +use threshold_crypto::{Signature, SignatureShare}; + +use crate::NodeId; + +#[derive(Serialize, Deserialize)] +pub struct LeaderRequest { + pub payload: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] +#[allow(clippy::large_enum_variant)] +pub enum LeaderResponse { + Ok { + #[serde(with = "hex_sig_share")] + signature: Signature, + }, + Err, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SigShareRequest { + pub payload: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SigShareResponse { + pub node_id: NodeId, + pub sig_share: SignatureShare, +} + +mod hex_sig_share { + use serde::{Deserialize, Deserializer, Serializer}; + use threshold_crypto::Signature; + + pub fn serialize(sig_share: &Signature, serializer: S) -> Result + where + S: Serializer, + { + let s = hex::encode(sig_share.to_bytes()); + serializer.serialize_str(&s) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Signature::from_bytes( + <[u8; 96]>::try_from(hex::decode(s).map_err(serde::de::Error::custom)?).map_err( + |v: Vec| { + serde::de::Error::custom(format!( + "signature has incorrect length: expected 96 bytes, but got {}", + v.len() + )) + }, + )?, + ) + .map_err(serde::de::Error::custom) + } +} diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs new file mode 100644 index 000000000..9e00d699f --- /dev/null +++ b/mpc-recovery/src/sign_node/mod.rs @@ -0,0 +1,49 @@ +use crate::msg::{SigShareRequest, SigShareResponse}; +use crate::NodeId; +use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; +use std::net::SocketAddr; +use threshold_crypto::{PublicKeySet, SecretKeyShare}; + +#[tracing::instrument(level = "debug", skip(pk_set, sk_share))] +pub async fn run(id: NodeId, pk_set: PublicKeySet, sk_share: SecretKeyShare, port: u16) { + tracing::debug!("running a sign node"); + + if pk_set.public_key_share(id) != sk_share.public_key_share() { + tracing::error!("provided secret share does not match the node id"); + return; + } + + let state = SignNodeState { id, sk_share }; + + let app = Router::new().route("/sign", post(sign)).with_state(state); + + let addr = SocketAddr::from(([0, 0, 0, 0], port)); + tracing::debug!(?addr, "starting http server"); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +#[derive(Clone)] +struct SignNodeState { + id: NodeId, + sk_share: SecretKeyShare, +} + +#[tracing::instrument(level = "debug", skip_all, fields(id = state.id))] +async fn sign( + State(state): State, + Json(request): Json, +) -> (StatusCode, Json) { + tracing::info!(payload = request.payload, "sign request"); + + // TODO: run some check that the payload makes sense, fail if not + tracing::debug!("approved"); + + let response = SigShareResponse { + node_id: state.id, + sig_share: state.sk_share.sign(request.payload), + }; + (StatusCode::OK, Json(response)) +} diff --git a/mpc-recovery/src/web.rs b/mpc-recovery/src/web.rs deleted file mode 100644 index 0b6901038..000000000 --- a/mpc-recovery/src/web.rs +++ /dev/null @@ -1,75 +0,0 @@ -use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; -use ractor::{concurrency::Duration, rpc::CallResult, ActorRef}; -use serde::Deserialize; -use std::net::SocketAddr; - -use crate::{ - actor::{NodeActor, NodeMessage}, - NodeId, -}; - -#[tracing::instrument(level = "debug", skip(node_actor))] -pub async fn serve(id: NodeId, port: u16, node_actor: ActorRef) { - let state = AppState { id, node_actor }; - - let app = Router::new() - .route("/submit", post(submit)) - .with_state(state); - - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - tracing::debug!(?addr, "starting a web server"); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -#[derive(Deserialize)] -struct SubmitPayload { - payload: String, -} - -#[derive(Clone)] -struct AppState { - id: NodeId, - node_actor: ActorRef, -} - -#[tracing::instrument(level = "debug", skip_all, fields(id = state.id))] -async fn submit( - State(state): State, - Json(payload): Json, -) -> (StatusCode, Json) { - tracing::info!(payload = payload.payload, "submit request"); - - match state - .node_actor - .call( - |tx| NodeMessage::NewRequest(payload.payload.bytes().collect(), tx), - Some(Duration::from_millis(2000)), - ) - .await - { - Ok(call_result) => match call_result { - CallResult::Success(sig_response) => ( - StatusCode::OK, - Json(hex::encode(sig_response.sig.to_bytes())), - ), - CallResult::Timeout => { - tracing::error!("failed due to timeout"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json("timeout".to_string()), - ) - } - CallResult::SenderError => { - tracing::error!("failed due to sender error (did not get a response)"); - (StatusCode::INTERNAL_SERVER_ERROR, Json("error".to_string())) - } - }, - Err(e) => { - tracing::error!("failed due to messaging error: {}", e); - (StatusCode::INTERNAL_SERVER_ERROR, Json("error".to_string())) - } - } -}