diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index d16144fc0..71a2d9bd3 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -46,4 +46,4 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - name: Test - run: cargo test -p mpc-recovery-integration-tests + run: cargo test -p mpc-recovery-integration-tests --jobs 1 -- --test-threads 1 diff --git a/DEPLOY.md b/DEPLOY.md index 347793128..89fd75378 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,36 +1,91 @@ # Manually Deploying mpc-recovery to GCP -GCP Project ID: pagoda-discovery-platform-dev -Service account: mpc-recovery@pagoda-discovery-platform-dev.iam.gserviceaccount.com +## Requirements -First, if you don't have credentials, go to [here](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106859519072057593233;edit=true/keys?project=pagoda-discovery-platform-dev) and generate a new one for yourself. +This guide assumes you have access to GCP console and the administrative ability to enable services, create service accounts and grant IAM roles if necessary. -Now, assuming you saved it as `mpc-recovery-creds.json` in the current working directory: +It is assumed that you have chosen a region to use throughout this guide. This can be any region, but we recommend something close to our leader node in `us-east1` if you are deploying production nodes. This region of your choosing will be referred to as `GCP_REGION`. -```bash -$ cat pagoda-discovery-platform-dev-92b300563d36.json | docker login -u _json_key --password-stdin https://us-east1-docker.pkg.dev +Make sure that: +* You have a GCP Project (its ID will be referred to as `GCP_PROJECT_ID` below, should look something like `pagoda-discovery-platform-dev`) +* `GCP_PROJECT_ID` has the following services enabled: + * `Artifact Registry` + * `Cloud Run` + * `Datastore` (should also be initialized with the default database) + * `Secret Manager` +* You have a service account dedicated to mpc-recovery (will be referred to as `GCP_SERVICE_ACCOUNT` below, should look something like `mpc-recovery@pagoda-discovery-platform-dev.iam.gserviceaccount.com`). +* `GCP_SERVICE_ACCOUNT` should have the following roles granted to it (change in `https://console.cloud.google.com/iam-admin/iam?project=`): + * `Artifact Registry Writer` + * `Cloud Datastore User` + * `Secret Manager Secret Accessor` + * `Cloud Run Admin` (TODO: might be able to downgrade to `Cloud Run Developer`) +* JSON service account keys for `GCP_SERVICE_ACCOUNT`. If you don't, then follow the steps below: + 1. Go to the service account page (`https://console.cloud.google.com/iam-admin/serviceaccounts?project=`) + 2. Select your `GCP_SERVICE_ACCOUNT` in the list + 3. Open `KEYS` tab + 4. Press `ADD KEY` and then `Create new key`. + 5. Choose `JSON` and press `CREATE`. + 6. Save the keys somewhere to your filesystem, we will refer to its location as `GCP_SERVICE_ACCOUNT_KEY_PATH`. + +## Configuration + +Your point of contact with Pagoda must have given you your Node ID (ask them if not). It is very important you use this specific ID for your node's configuration, we will refer to this value as `MPC_NODE_ID`. + +[TODO]: <> (Change key serialization format to a more conventional format so that users can generate it outside of mpc-recovery) + +You also need a Ed25519 key pair that you can generate by running `cargo run -- generate 1` in this directory. Grab JSON object after `Secret key share 0:`; it should look like this: +```json +{"public_key":{"curve":"ed25519","point":[120,153,87,73,144,228,107,221,163,76,41,132,123,208,73,71,110,235,204,191,174,106,225,69,38,145,165,76,132,201,55,152]},"expanded_private_key":{"prefix":{"curve":"ed25519","scalar":[180,110,118,232,35,24,127,100,6,137,244,195,8,154,150,22,214,43,134,73,234,67,255,249,99,157,120,6,163,88,178,12]},"private_key":{"curve":"ed25519","scalar":[160,85,170,73,186,103,158,30,156,142,160,162,253,246,210,214,173,162,39,244,145,241,58,148,63,211,218,241,11,70,235,89]}}} ``` -This will log you into the GCP Artifact Repository. +Now save it to GCP Secret Manager under the name of your choosing (e.g. `mpc-recovery-key-prod`). This name will be referred to as `GCP_SM_KEY_NAME`. + +## Uploading Docker Image + +First, let's create a new repository in GCP Artifact Registry. Go to `https://console.cloud.google.com/artifacts?project=`, press `CREATE REPOSITORY` and follow the form to create a new repository with **Docker** format and **Standard** mode. Name can be anything we will refer to it as `GCP_ARTIFACT_REPO`. + +Now, you need to log into the GCP Artifact Registry on your machine: -Build the mpc-recovery docker image like you usually would, but tag it with this image name: +```bash +$ cat | docker login -u _json_key --password-stdin https://-docker.pkg.dev +``` + +Build the mpc-recovery docker image from this folder and make sure to tag it with this image name: ```bash -$ docker build . -t us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery-tmp/mpc-recovery +$ docker build . -t -docker.pkg.dev///mpc-recovery ``` Push the image to GCP Artifact Registry: ```bash -$ docker push us-east1-docker.pkg.dev/pagoda-discovery-platform-dev/mpc-recovery-tmp/mpc-recovery +$ docker push -docker.pkg.dev///mpc-recovery ``` - You can check that the image has been successfully uploaded [here](https://console.cloud.google.com/artifacts/docker/pagoda-discovery-platform-dev/us-east1/mpc-recovery-tmp?project=pagoda-discovery-platform-dev). +You can check that the image has been successfully uploaded on the GCP Artifact Registry dashboard. + +## Running on Cloud Run - Now reset the VM instance: - - ```bash - $ gcloud compute instances reset mpc-recovery-tmp-0 - ``` +Pick a name for your Cloud Run service, we will refer to it as `GCP_CLOUD_RUN_SERVICE`. For example `mpc-signer-pagoda-prod`. + +Run: + +```bash +$ gcloud run deploy \ + --image=-docker.pkg.dev///mpc-recovery \ + --allow-unauthenticated \ + --port=3000 \ + --args=start-sign \ + --service-account= \ + --cpu=2 \ + --memory=2Gi \ + --min-instances=1 \ + --max-instances=1 \ + --set-env-vars=MPC_RECOVERY_NODE_ID=,MPC_RECOVERY_WEB_PORT=3000,RUST_LOG=mpc_recovery=debug,MPC_RECOVERY_GCP_PROJECT_ID= \ + --set-secrets=MPC_RECOVERY_SK_SHARE=:latest \ + --no-cpu-throttling \ + --region= \ + --project= +``` - The API should be available shortly on `http://34.139.85.130:3000`. +If deploy ends successfully it will give you a Service URL, share it with your Pagoda point of contact. diff --git a/integration-tests/tests/docker/mod.rs b/integration-tests/tests/docker/mod.rs index 10a2b8a21..56dabd8d8 100644 --- a/integration-tests/tests/docker/mod.rs +++ b/integration-tests/tests/docker/mod.rs @@ -8,7 +8,6 @@ use bollard::{ service::{HostConfig, Ipam, PortBinding}, Docker, }; -use curv::elliptic::curves::{Ed25519, Point}; use futures::{lock::Mutex, StreamExt}; use hyper::{Body, Client, Method, Request, StatusCode, Uri}; use mpc_recovery::msg::{AddKeyRequest, AddKeyResponse, NewAccountRequest, NewAccountResponse}; @@ -283,7 +282,6 @@ impl SignNode { docker: &Docker, network: &str, node_id: u64, - pk_set: &Vec>, sk_share: &ExpandedKeyPair, datastore_url: &str, gcp_project_id: &str, @@ -295,8 +293,6 @@ impl SignNode { "start-sign".to_string(), "--node-id".to_string(), node_id.to_string(), - "--pk-set".to_string(), - serde_json::to_string(&pk_set)?, "--sk-share".to_string(), serde_json::to_string(&sk_share)?, "--web-port".to_string(), diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs index 5438e52d4..23d753956 100644 --- a/integration-tests/tests/lib.rs +++ b/integration-tests/tests/lib.rs @@ -80,7 +80,6 @@ where &docker, NETWORK, i as u64, - &pk_set, share, &datastore.address, GCP_PROJECT_ID, diff --git a/integration-tests/tests/mpc/negative.rs b/integration-tests/tests/mpc/negative.rs index 63098a300..7b365859a 100644 --- a/integration-tests/tests/mpc/negative.rs +++ b/integration-tests/tests/mpc/negative.rs @@ -5,7 +5,7 @@ use std::time::Duration; #[tokio::test] async fn test_invalid_token() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(1, |ctx| { Box::pin(async move { let account_id = account::random(ctx.worker)?; let user_public_key = key::random(); @@ -87,7 +87,7 @@ async fn test_invalid_token() -> anyhow::Result<()> { #[tokio::test] async fn test_malformed_account_id() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(1, |ctx| { Box::pin(async move { let malformed_account_id = account::malformed(); let user_public_key = key::random(); @@ -170,7 +170,7 @@ async fn test_malformed_account_id() -> anyhow::Result<()> { #[tokio::test] async fn test_malformed_public_key() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(1, |ctx| { Box::pin(async move { let account_id = account::random(ctx.worker)?; let malformed_public_key = key::malformed(); @@ -255,7 +255,7 @@ async fn test_malformed_public_key() -> anyhow::Result<()> { #[tokio::test] async fn test_add_key_to_non_existing_account() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(1, |ctx| { Box::pin(async move { let account_id = account::random(ctx.worker)?; let user_public_key = key::random(); diff --git a/integration-tests/tests/mpc/positive.rs b/integration-tests/tests/mpc/positive.rs index f12fa77c3..0aed9c146 100644 --- a/integration-tests/tests/mpc/positive.rs +++ b/integration-tests/tests/mpc/positive.rs @@ -11,7 +11,7 @@ use std::time::Duration; #[tokio::test] async fn test_trio() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(3, |ctx| { Box::pin(async move { let payload: String = rand::thread_rng() .sample_iter(&Alphanumeric) @@ -50,7 +50,7 @@ async fn test_trio() -> anyhow::Result<()> { // TODO: write a test with real token #[tokio::test] async fn test_basic_action() -> anyhow::Result<()> { - with_nodes(4, |ctx| { + with_nodes(3, |ctx| { Box::pin(async move { let account_id = account::random(ctx.worker)?; let user_public_key = key::random(); diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 1c65a15d0..720d2753f 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -1,5 +1,8 @@ use crate::key_recovery::get_user_recovery_pk; -use crate::msg::{AddKeyRequest, AddKeyResponse, NewAccountRequest, NewAccountResponse}; +use crate::msg::{ + AcceptNodePublicKeysRequest, AddKeyRequest, AddKeyResponse, NewAccountRequest, + NewAccountResponse, +}; use crate::nar; use crate::oauth::OAuthTokenVerifier; use crate::relayer::error::RelayerError; @@ -10,6 +13,7 @@ use crate::transaction::{ get_local_signed_delegated_action, get_mpc_signed_delegated_action, }; use axum::{http::StatusCode, routing::post, Extension, Json, Router}; +use curv::elliptic::curves::{Ed25519, Point}; use near_crypto::{ParseKeyError, PublicKey, SecretKey}; use near_primitives::account::id::ParseAccountError; use near_primitives::types::AccountId; @@ -77,6 +81,24 @@ pub async fn run(config: Config) { pagoda_firebase_audience_id, }; + // Get keys from all sign nodes, and broadcast them out as a set. + let pk_set = match gather_sign_node_pks(&state).await { + Ok(pk_set) => pk_set, + Err(err) => { + tracing::error!("Unable to gather public keys: {err}"); + return; + } + }; + tracing::debug!(?pk_set, "Gathered public keys"); + let messages = match broadcast_pk_set(&state, pk_set).await { + Ok(messages) => messages, + Err(err) => { + tracing::error!("Unable to broadcast public keys: {err}"); + return; + } + }; + tracing::debug!(?messages, "broadcasted public key statuses"); + //TODO: not secure, allow only for testnet, whitelist endpoint etc. for mainnet let cors_layer = tower_http::cors::CorsLayer::permissive(); @@ -440,6 +462,56 @@ async fn add_key( } } } + +async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result>> { + let fut = nar::retry_every(std::time::Duration::from_secs(1), || async { + let results: anyhow::Result)>> = crate::transaction::call( + &state.reqwest_client, + &state.sign_nodes, + "public_key_node", + (), + ) + .await; + let mut results = match results { + Ok(results) => results, + Err(err) => { + tracing::debug!("failed to gather pk: {err}"); + return Err(err); + } + }; + + results.sort_by_key(|(index, _)| *index); + let results: Vec> = + results.into_iter().map(|(_index, point)| point).collect(); + + anyhow::Result::Ok(results) + }); + + let results = tokio::time::timeout(std::time::Duration::from_secs(60), fut) + .await + .map_err(|_| anyhow::anyhow!("timeout gathering sign node pks"))??; + Ok(results) +} + +async fn broadcast_pk_set( + state: &LeaderState, + pk_set: Vec>, +) -> anyhow::Result> { + let request = AcceptNodePublicKeysRequest { + public_keys: pk_set, + }; + + let messages: Vec = crate::transaction::call( + &state.reqwest_client, + &state.sign_nodes, + "accept_pk_set", + request, + ) + .await?; + + Ok(messages) +} + #[cfg(test)] mod tests { use super::*; diff --git a/mpc-recovery/src/main.rs b/mpc-recovery/src/main.rs index e5f6aca11..1eef6fc18 100644 --- a/mpc-recovery/src/main.rs +++ b/mpc-recovery/src/main.rs @@ -1,5 +1,4 @@ use clap::Parser; -use curv::elliptic::curves::{Ed25519, Point}; use mpc_recovery::{ gcp::GcpService, oauth::{PagodaFirebaseTokenVerifier, UniversalTokenVerifier}, @@ -71,9 +70,6 @@ enum Cli { /// Node ID #[arg(long, env("MPC_RECOVERY_NODE_ID"))] node_id: u64, - /// Root public key - #[arg(long, env("MPC_RECOVERY_PK_SET"))] - pk_set: String, /// Secret key share, will be pulled from GCP Secret Manager if omitted #[arg(long, env("MPC_RECOVERY_SK_SHARE"))] sk_share: Option, @@ -194,7 +190,6 @@ async fn main() -> anyhow::Result<()> { Cli::StartSign { env, node_id, - pk_set, sk_share, web_port, gcp_project_id, @@ -205,8 +200,6 @@ async fn main() -> anyhow::Result<()> { GcpService::new(env.clone(), gcp_project_id, gcp_datastore_url).await?; let sk_share = load_sh_skare(&gcp_service, &env, node_id, sk_share).await?; - // TODO put these in a better defined format - let pk_set: Vec> = serde_json::from_str(&pk_set).unwrap(); // TODO Import just the private key and derive the rest let sk_share: ExpandedKeyPair = serde_json::from_str(&sk_share).unwrap(); @@ -214,7 +207,6 @@ async fn main() -> anyhow::Result<()> { mpc_recovery::run_sign_node::( gcp_service, node_id, - pk_set, sk_share, web_port, ) @@ -223,7 +215,6 @@ async fn main() -> anyhow::Result<()> { mpc_recovery::run_sign_node::( gcp_service, node_id, - pk_set, sk_share, web_port, ) diff --git a/mpc-recovery/src/msg.rs b/mpc-recovery/src/msg.rs index 6e2893000..be1bbbe5c 100644 --- a/mpc-recovery/src/msg.rs +++ b/mpc-recovery/src/msg.rs @@ -1,3 +1,4 @@ +use curv::elliptic::curves::{Ed25519, Point}; use ed25519_dalek::Signature; use serde::{Deserialize, Serialize}; @@ -77,6 +78,11 @@ pub struct SigShareRequest { pub payload: Vec, } +#[derive(Serialize, Deserialize, Debug)] +pub struct AcceptNodePublicKeysRequest { + pub public_keys: Vec>, +} + mod hex_sig_share { use ed25519_dalek::Signature; use serde::{Deserialize, Deserializer, Serializer}; diff --git a/mpc-recovery/src/nar.rs b/mpc-recovery/src/nar.rs index 62eb3d657..d647dc5d2 100644 --- a/mpc-recovery/src/nar.rs +++ b/mpc-recovery/src/nar.rs @@ -6,6 +6,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; use near_crypto::PublicKey; use near_jsonrpc_client::errors::{JsonRpcError, JsonRpcServerError}; @@ -23,6 +24,16 @@ use crate::relayer::error::RelayerError; pub(crate) type CachedAccessKeyNonces = RwLock>; +pub(crate) async fn retry_every(interval: Duration, task: F) -> T::Output +where + F: FnMut() -> T, + T: core::future::Future>, +{ + let retry_strategy = std::iter::repeat_with(|| interval); + let task = Retry::spawn(retry_strategy, task); + task.await +} + pub(crate) async fn retry(task: F) -> T::Output where F: FnMut() -> T, diff --git a/mpc-recovery/src/sign_node/aggregate_signer.rs b/mpc-recovery/src/sign_node/aggregate_signer.rs index ecd678349..255805996 100644 --- a/mpc-recovery/src/sign_node/aggregate_signer.rs +++ b/mpc-recovery/src/sign_node/aggregate_signer.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; +use std::sync::Arc; use curv::arithmetic::Converter; use curv::cryptographic_primitives::commitments::{ @@ -13,6 +14,7 @@ use multi_party_eddsa::protocols::aggsig::{self, KeyAgg, SignSecondMsg}; use rand8::rngs::OsRng; use rand8::Rng; use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; use crate::transaction::{to_dalek_public_key, to_dalek_signature}; @@ -59,7 +61,7 @@ impl SigningState { Ok(commitment) } - pub fn get_reveal( + pub async fn get_reveal( &mut self, node_info: NodeInfo, recieved_commitments: Vec, @@ -79,7 +81,7 @@ impl SigningState { .remove(&our_c.commitment) .ok_or(format!("Committment {:?} not found", &our_c.commitment))?; - let (reveal, state) = state.reveal(&node_info, recieved_commitments)?; + let (reveal, state) = state.reveal(&node_info, recieved_commitments).await?; let reveal = Reveal(reveal); self.revealed.insert(reveal.clone(), state); Ok(reveal) @@ -166,13 +168,14 @@ impl Committed { Ok((sc, s)) } - pub fn reveal( + pub async fn reveal( self, node_info: &NodeInfo, commitments: Vec, ) -> Result<(SignSecondMsg, Revealed), String> { let (commitments, signing_public_keys) = node_info - .signed_by_every_node(commitments)? + .signed_by_every_node(commitments) + .await? .into_iter() .unzip(); Ok(( @@ -227,16 +230,27 @@ impl Revealed { // Stores info about the other nodes we're interacting with #[derive(Clone)] pub struct NodeInfo { - pub nodes_public_keys: Vec>, + pub nodes_public_keys: Arc>>>>, pub our_index: usize, } impl NodeInfo { - fn signed_by_every_node( + pub fn new(our_index: usize, nodes_public_keys: Option>>) -> Self { + Self { + our_index, + nodes_public_keys: Arc::new(RwLock::new(nodes_public_keys)), + } + } + + async fn signed_by_every_node( &self, signed: Vec, ) -> Result)>, String> { self.nodes_public_keys + .read() + .await + .as_ref() + .ok_or_else(|| "No nodes public keys available to sign".to_string())? .iter() .zip(signed.iter()) .map(|(public_key, signed)| signed.verify(public_key)) @@ -320,8 +334,8 @@ mod tests { use ed25519_dalek::{SignatureError, Verifier}; use multi_party_eddsa::protocols::ExpandedKeyPair; - #[test] - fn aggregate_signatures() { + #[tokio::test] + async fn aggregate_signatures() { pub fn verify_dalek( pk: &Point, sig: &protocols::Signature, @@ -349,10 +363,7 @@ mod tests { n3.public_key.clone(), ]; - let ni = |n| NodeInfo { - nodes_public_keys: nodes_public_keys.clone(), - our_index: n, - }; + let ni = |n| NodeInfo::new(n, Some(nodes_public_keys.clone())); // Set up nodes with that config let mut s1 = SigningState::new(); @@ -368,9 +379,9 @@ mod tests { ]; let reveals = vec![ - s1.get_reveal(ni(0), commitments.clone()).unwrap(), - s2.get_reveal(ni(1), commitments.clone()).unwrap(), - s3.get_reveal(ni(2), commitments.clone()).unwrap(), + s1.get_reveal(ni(0), commitments.clone()).await.unwrap(), + s2.get_reveal(ni(1), commitments.clone()).await.unwrap(), + s3.get_reveal(ni(2), commitments.clone()).await.unwrap(), ]; let sig_shares = vec![ diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs index 334ae62e7..89b98ffec 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -1,9 +1,10 @@ use self::aggregate_signer::{NodeInfo, Reveal, SignedCommitment, SigningState}; use self::user_credentials::UserCredentials; use crate::gcp::GcpService; -use crate::msg::SigShareRequest; +use crate::msg::{AcceptNodePublicKeysRequest, SigShareRequest}; use crate::oauth::OAuthTokenVerifier; use crate::primitives::InternalAccountId; +use crate::sign_node::pk_set::SignerNodePkSet; use crate::NodeId; use axum::{http::StatusCode, routing::post, Extension, Json, Router}; use curv::elliptic::curves::{Ed25519, Point}; @@ -13,37 +14,32 @@ use std::sync::Arc; use tokio::sync::RwLock; pub mod aggregate_signer; +pub mod pk_set; pub mod user_credentials; -#[tracing::instrument(level = "debug", skip(gcp_service, node_key, nodes_public_keys))] +#[tracing::instrument(level = "debug", skip(gcp_service, node_key))] pub async fn run( gcp_service: GcpService, our_index: NodeId, - nodes_public_keys: Vec>, node_key: ExpandedKeyPair, port: u16, ) { tracing::debug!("running a sign node"); let our_index = usize::try_from(our_index).expect("This index is way to big"); - if nodes_public_keys.get(our_index) != Some(&node_key.public_key) { - tracing::error!("provided secret share does not match the node id"); - return; - } + let pk_set = gcp_service + .get::<_, SignerNodePkSet>(format!("{}/{}", our_index, pk_set::MAIN_KEY)) + .await + .unwrap_or_default(); let pagoda_firebase_audience_id = "pagoda-firebase-audience-id".to_string(); - let signing_state = Arc::new(RwLock::new(SigningState::new())); - let state = SignNodeState { gcp_service, node_key, signing_state, pagoda_firebase_audience_id, - node_info: NodeInfo { - nodes_public_keys, - our_index, - }, + node_info: NodeInfo::new(our_index, pk_set.map(|set| set.public_keys)), }; let app = Router::new() @@ -51,6 +47,8 @@ pub async fn run( .route("/reveal", post(reveal)) .route("/signature_share", post(signature_share)) .route("/public_key", post(public_key)) + .route("/public_key_node", post(public_key_node)) + .route("/accept_pk_set", post(accept_pk_set)) .layer(Extension(state)); let addr = SocketAddr::from(([0, 0, 0, 0], port)); @@ -144,6 +142,10 @@ async fn commit( Extension(state): Extension, Json(request): Json, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state).await { + return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); + } + match process_commit::(state, request).await { Ok(signed_commitment) => (StatusCode::OK, Json(Ok(signed_commitment))), Err(ref e @ CommitError::OidcVerificationFailed(ref err_msg)) => { @@ -168,11 +170,16 @@ async fn reveal( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state).await { + return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); + } + match state .signing_state .write() .await .get_reveal(state.node_info, request) + .await { Ok(r) => { tracing::debug!("Successful reveal"); @@ -190,6 +197,10 @@ async fn signature_share( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state).await { + return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); + } + match state .signing_state .write() @@ -229,3 +240,70 @@ async fn public_key( } } } + +// TODO: remove type complexity +#[allow(clippy::type_complexity)] +#[tracing::instrument(level = "debug", skip_all, fields(id = state.node_info.our_index))] +async fn public_key_node( + Extension(state): Extension, + Json(_): Json<()>, +) -> (StatusCode, Json), String>>) { + ( + StatusCode::OK, + Json(Ok((state.node_info.our_index, state.node_key.public_key))), + ) +} + +#[tracing::instrument(level = "debug", skip_all, fields(id = state.node_info.our_index))] +async fn accept_pk_set( + Extension(state): Extension, + Json(request): Json, +) -> (StatusCode, Json>) { + let index = state.node_info.our_index; + if request.public_keys.get(index) != Some(&state.node_key.public_key) { + tracing::error!("provided secret share does not match the node id"); + return (StatusCode::BAD_REQUEST, Json(Err(format!( + "Sign node could not accept the public keys: current node index={index} does not match up")))); + } + + let mut public_keys = state.node_info.nodes_public_keys.write().await; + if public_keys.is_some() { + return ( + StatusCode::BAD_REQUEST, + Json(Err( + "This node is already initialized with public keys".to_string() + )), + ); + } + tracing::debug!("Setting node public keys => {:?}", request.public_keys); + public_keys.replace(request.public_keys.clone()); + match state + .gcp_service + .insert(SignerNodePkSet { + node_id: state.node_info.our_index, + public_keys: request.public_keys, + }) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(Ok("Successfully set node public keys".to_string())), + ), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(Ok("failed to save the keys".to_string())), + ), + } +} + +/// Validate whether the current state of the sign node is useable or not. +async fn check_if_ready(state: &SignNodeState) -> Result<(), String> { + let public_keys = state.node_info.nodes_public_keys.read().await; + if public_keys.is_none() { + return Err( + "Sign node is not ready yet: waiting on all public keys from leader node".into(), + ); + } + + Ok(()) +} diff --git a/mpc-recovery/src/sign_node/pk_set.rs b/mpc-recovery/src/sign_node/pk_set.rs new file mode 100644 index 000000000..457d7addb --- /dev/null +++ b/mpc-recovery/src/sign_node/pk_set.rs @@ -0,0 +1,76 @@ +use crate::gcp::{ + error::ConvertError, + value::{FromValue, IntoValue, Value}, + KeyKind, +}; +use curv::elliptic::curves::{Ed25519, Point}; +use google_datastore1::api::{Key, PathElement}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub const MAIN_KEY: &str = "main"; + +#[derive(Serialize, Deserialize, Clone)] +pub struct SignerNodePkSet { + pub node_id: usize, + pub public_keys: Vec>, +} + +impl KeyKind for SignerNodePkSet { + fn kind() -> String { + "SignerNodePkSet".to_string() + } +} + +impl IntoValue for SignerNodePkSet { + fn into_value(self) -> Value { + let mut properties = HashMap::new(); + properties.insert( + "node_id".to_string(), + Value::IntegerValue(self.node_id as i64), + ); + properties.insert( + "public_keys".to_string(), + Value::StringValue(serde_json::to_string(&self.public_keys).unwrap()), + ); + Value::EntityValue { + key: Key { + path: Some(vec![PathElement { + kind: Some(SignerNodePkSet::kind()), + name: Some(format!("{}/{}", self.node_id, MAIN_KEY)), + id: None, + }]), + partition_id: None, + }, + properties, + } + } +} + +impl FromValue for SignerNodePkSet { + fn from_value(value: Value) -> Result { + match value { + Value::EntityValue { mut properties, .. } => { + let (_, node_id) = properties + .remove_entry("node_id") + .ok_or_else(|| ConvertError::MissingProperty("node_id".to_string()))?; + let node_id = i64::from_value(node_id)? as usize; + let (_, public_keys) = properties + .remove_entry("public_keys") + .ok_or_else(|| ConvertError::MissingProperty("public_keys".to_string()))?; + let public_keys = String::from_value(public_keys)?; + let public_keys = serde_json::from_str(&public_keys) + .map_err(|_| ConvertError::MalformedProperty("public_keys".to_string()))?; + + Ok(Self { + node_id, + public_keys, + }) + } + value => Err(ConvertError::UnexpectedPropertyType { + expected: "entity".to_string(), + got: format!("{:?}", value), + }), + } + } +}