From 56ff149af0058ba2244f1d983b0eecc93ee1ec99 Mon Sep 17 00:00:00 2001 From: Phuong Nguyen Date: Mon, 24 Apr 2023 16:16:32 -0700 Subject: [PATCH 01/10] Initial attempt at distributing public keys --- integration-tests/tests/docker/mod.rs | 4 - integration-tests/tests/lib.rs | 1 - mpc-recovery/src/leader_node/mod.rs | 41 ++++++++++ mpc-recovery/src/main.rs | 9 +-- mpc-recovery/src/msg.rs | 6 ++ .../src/sign_node/aggregate_signer.rs | 6 +- mpc-recovery/src/sign_node/mod.rs | 76 ++++++++++++++----- 7 files changed, 111 insertions(+), 32 deletions(-) diff --git a/integration-tests/tests/docker/mod.rs b/integration-tests/tests/docker/mod.rs index 9bf2d6364..ce8748353 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}; @@ -285,7 +284,6 @@ impl SignNode { docker: &Docker, network: &str, node_id: u64, - pk_set: &Vec>, sk_share: &ExpandedKeyPair, datastore_url: &str, gcp_project_id: &str, @@ -297,8 +295,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 ff40391a5..9c3888ad3 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/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index b2b0e4ac9..133715ff6 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -10,6 +10,7 @@ use crate::transaction::{ }; use crate::{nar, NodeId}; 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; @@ -76,6 +77,17 @@ pub async fn run(config: Config) { pagoda_firebase_audience_id, }; + // Get keys from all sign nodes, and broadcast them out as a set. + let Ok(pk_set) = gather_sign_node_pks(&state).await else { + tracing::error!("Unable to gather public keys"); + return; + }; + let Ok(messages) = broadcast_pk_set(&state, pk_set).await else { + tracing::error!("Unable to broadcast public keys"); + 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(); @@ -439,6 +451,35 @@ async fn add_key( } } } + +async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result>> { + let mut results: Vec<(usize, Point)> = crate::transaction::call( + &state.reqwest_client, + &state.sign_nodes, + "public_key_node", + (), + ) + .await?; + results.sort_by_key(|(index, _)| *index); + let results = results.into_iter().map(|(_index, point)| point).collect(); + Ok(results) +} + +async fn broadcast_pk_set( + state: &LeaderState, + pk_set: Vec>, +) -> anyhow::Result> { + let messages: Vec = crate::transaction::call( + &state.reqwest_client, + &state.sign_nodes, + "accept_pk_set", + pk_set, + ) + .await?; + + Ok(messages) +} + #[cfg(test)] mod tests { use super::*; diff --git a/mpc-recovery/src/main.rs b/mpc-recovery/src/main.rs index 4346a078e..4ac0733f8 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, LeaderConfig}; use multi_party_eddsa::protocols::ExpandedKeyPair; use near_primitives::types::AccountId; @@ -61,9 +60,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, @@ -166,7 +162,6 @@ async fn main() -> anyhow::Result<()> { } Cli::StartSign { node_id, - pk_set, sk_share, web_port, gcp_project_id, @@ -175,12 +170,10 @@ async fn main() -> anyhow::Result<()> { let gcp_service = GcpService::new(gcp_project_id, gcp_datastore_url).await?; let sk_share = load_sh_skare(&gcp_service, 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(); - mpc_recovery::run_sign_node(gcp_service, node_id, pk_set, sk_share, web_port).await; + mpc_recovery::run_sign_node(gcp_service, node_id, sk_share, web_port).await; } } 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/sign_node/aggregate_signer.rs b/mpc-recovery/src/sign_node/aggregate_signer.rs index ecd678349..8e661debd 100644 --- a/mpc-recovery/src/sign_node/aggregate_signer.rs +++ b/mpc-recovery/src/sign_node/aggregate_signer.rs @@ -227,7 +227,7 @@ 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: Option>>, pub our_index: usize, } @@ -237,6 +237,8 @@ impl NodeInfo { signed: Vec, ) -> Result)>, String> { self.nodes_public_keys + .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)) @@ -350,7 +352,7 @@ mod tests { ]; let ni = |n| NodeInfo { - nodes_public_keys: nodes_public_keys.clone(), + nodes_public_keys: Some(nodes_public_keys.clone()), our_index: n, }; diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs index 734a94781..f7a582bc0 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -1,7 +1,7 @@ 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, UniversalTokenVerifier}; use crate::primitives::InternalAccountId; use crate::NodeId; @@ -15,33 +15,20 @@ use tokio::sync::RwLock; pub mod aggregate_signer; pub mod user_credentials; -#[tracing::instrument(level = "debug", skip(gcp_service, node_key, nodes_public_keys))] -pub async fn run( - gcp_service: GcpService, - our_index: NodeId, - nodes_public_keys: Vec>, - node_key: ExpandedKeyPair, - port: u16, -) { +#[tracing::instrument(level = "debug", skip(gcp_service, node_key))] +pub async fn run(gcp_service: GcpService, our_index: NodeId, 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 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, + nodes_public_keys: None, our_index, }, }; @@ -51,6 +38,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)); @@ -135,6 +124,10 @@ async fn commit( Extension(state): Extension, Json(request): Json, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state) { + 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)) => { @@ -159,6 +152,10 @@ async fn reveal( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state) { + return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); + } + match state .signing_state .write() @@ -181,6 +178,10 @@ async fn signature_share( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { + if let Err(msg) = check_if_ready(&state) { + return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); + } + match state .signing_state .write() @@ -219,3 +220,44 @@ async fn public_key( } } } + +#[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<(usize, Point)>) { + ( + StatusCode::OK, + Json((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(mut 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(format!( + "Sign node could not accept the public keys: current node index={index} does not match up"))); + } + + state.node_info.nodes_public_keys = Some(request.public_keys); + ( + StatusCode::OK, + Json("Successfully set node public keys".to_string()), + ) +} + +/// Validate whether the current state of the sign node is useable or not. +fn check_if_ready(state: &SignNodeState) -> Result<(), String> { + if state.node_info.nodes_public_keys.is_none() { + return Err( + "Sign node is not ready yet: waiting on all public keys from leader node".into(), + ); + } + + Ok(()) +} From 2b5530dd217d88a193d964c60c49b3ca3333cab3 Mon Sep 17 00:00:00 2001 From: Phuong Nguyen Date: Mon, 24 Apr 2023 18:23:05 -0700 Subject: [PATCH 02/10] Added retrying for gathering nodes public keys until they're online --- mpc-recovery/src/leader_node/mod.rs | 32 +++++++++++++++++++++-------- mpc-recovery/src/nar.rs | 11 ++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 133715ff6..4c8038ced 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -453,15 +453,29 @@ async fn add_key( } async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result>> { - let mut results: Vec<(usize, Point)> = crate::transaction::call( - &state.reqwest_client, - &state.sign_nodes, - "public_key_node", - (), - ) - .await?; - results.sort_by_key(|(index, _)| *index); - let results = results.into_iter().map(|(_index, point)| point).collect(); + let fut = nar::retry_every(std::time::Duration::from_millis(250), || 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) => 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(10), fut) + .await + .map_err(|_| anyhow::anyhow!("timeout gathering sign node pks"))??; Ok(results) } diff --git a/mpc-recovery/src/nar.rs b/mpc-recovery/src/nar.rs index 62eb3d657..71e829cde 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.clone()); + let task = Retry::spawn(retry_strategy, task); + task.await +} + pub(crate) async fn retry(task: F) -> T::Output where F: FnMut() -> T, From a86bb0f58fc9922509b8f2c0107f137fbaca386a Mon Sep 17 00:00:00 2001 From: Phuong Nguyen Date: Mon, 24 Apr 2023 19:06:50 -0700 Subject: [PATCH 03/10] Made node_info.public_keys into rwlock --- mpc-recovery/src/nar.rs | 2 +- .../src/sign_node/aggregate_signer.rs | 17 ++++++++---- mpc-recovery/src/sign_node/mod.rs | 26 +++++++++++-------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/mpc-recovery/src/nar.rs b/mpc-recovery/src/nar.rs index 71e829cde..d647dc5d2 100644 --- a/mpc-recovery/src/nar.rs +++ b/mpc-recovery/src/nar.rs @@ -29,7 +29,7 @@ where F: FnMut() -> T, T: core::future::Future>, { - let retry_strategy = std::iter::repeat_with(|| interval.clone()); + let retry_strategy = std::iter::repeat_with(|| interval); let task = Retry::spawn(retry_strategy, task); task.await } diff --git a/mpc-recovery/src/sign_node/aggregate_signer.rs b/mpc-recovery/src/sign_node/aggregate_signer.rs index 8e661debd..722ed9539 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, RwLock}; use curv::arithmetic::Converter; use curv::cryptographic_primitives::commitments::{ @@ -227,16 +228,25 @@ impl Revealed { // Stores info about the other nodes we're interacting with #[derive(Clone)] pub struct NodeInfo { - pub nodes_public_keys: Option>>, + pub nodes_public_keys: Arc>>>>, pub our_index: usize, } impl NodeInfo { + pub fn new(our_index: usize, nodes_public_keys: Option>>) -> Self { + Self { + our_index, + nodes_public_keys: Arc::new(RwLock::new(nodes_public_keys)), + } + } + fn signed_by_every_node( &self, signed: Vec, ) -> Result)>, String> { self.nodes_public_keys + .read() + .map_err(|e| e.to_string())? .as_ref() .ok_or_else(|| "No nodes public keys available to sign".to_string())? .iter() @@ -351,10 +361,7 @@ mod tests { n3.public_key.clone(), ]; - let ni = |n| NodeInfo { - nodes_public_keys: Some(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(); diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs index f7a582bc0..3144db5c0 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -27,10 +27,7 @@ pub async fn run(gcp_service: GcpService, our_index: NodeId, node_key: ExpandedK node_key, signing_state, pagoda_firebase_audience_id, - node_info: NodeInfo { - nodes_public_keys: None, - our_index, - }, + node_info: NodeInfo::new(our_index, None), }; let app = Router::new() @@ -124,7 +121,7 @@ async fn commit( Extension(state): Extension, Json(request): Json, ) -> (StatusCode, Json>) { - if let Err(msg) = check_if_ready(&state) { + if let Err(msg) = check_if_ready(&state).await { return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); } @@ -152,7 +149,7 @@ async fn reveal( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { - if let Err(msg) = check_if_ready(&state) { + if let Err(msg) = check_if_ready(&state).await { return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); } @@ -178,7 +175,7 @@ async fn signature_share( Extension(state): Extension, Json(request): Json>, ) -> (StatusCode, Json>) { - if let Err(msg) = check_if_ready(&state) { + if let Err(msg) = check_if_ready(&state).await { return (StatusCode::INTERNAL_SERVER_ERROR, Json(Err(msg))); } @@ -234,7 +231,7 @@ async fn public_key_node( #[tracing::instrument(level = "debug", skip_all, fields(id = state.node_info.our_index))] async fn accept_pk_set( - Extension(mut state): Extension, + Extension(state): Extension, Json(request): Json, ) -> (StatusCode, Json) { let index = state.node_info.our_index; @@ -244,7 +241,10 @@ async fn accept_pk_set( "Sign node could not accept the public keys: current node index={index} does not match up"))); } - state.node_info.nodes_public_keys = Some(request.public_keys); + let Ok(mut public_keys) = state.node_info.nodes_public_keys.write() else { + return (StatusCode::INTERNAL_SERVER_ERROR, Json("Unable to write into public keys. RwLock potentially poisoned".into())); + }; + *public_keys = Some(request.public_keys); ( StatusCode::OK, Json("Successfully set node public keys".to_string()), @@ -252,8 +252,12 @@ async fn accept_pk_set( } /// Validate whether the current state of the sign node is useable or not. -fn check_if_ready(state: &SignNodeState) -> Result<(), String> { - if state.node_info.nodes_public_keys.is_none() { +async fn check_if_ready(state: &SignNodeState) -> Result<(), String> { + let Ok(public_keys) = state.node_info.nodes_public_keys.read() else { + return Err("Unable to check public keys. RwLock potentially poisoned".into()); + }; + + if public_keys.is_none() { return Err( "Sign node is not ready yet: waiting on all public keys from leader node".into(), ); From 3fba31ecf60be56a3eedc64cd21a83fae81f6d24 Mon Sep 17 00:00:00 2001 From: Phuong Nguyen Date: Mon, 24 Apr 2023 22:24:52 -0700 Subject: [PATCH 04/10] Fixed pk_set deserialization errors --- mpc-recovery/src/leader_node/mod.rs | 35 ++++++++++++++----- .../src/sign_node/aggregate_signer.rs | 26 +++++++------- mpc-recovery/src/sign_node/mod.rs | 27 +++++++------- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 4c8038ced..503e91629 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::oauth::{OAuthTokenVerifier, UniversalTokenVerifier}; use crate::relayer::error::RelayerError; use crate::relayer::msg::RegisterAccountRequest; @@ -78,13 +81,20 @@ pub async fn run(config: Config) { }; // Get keys from all sign nodes, and broadcast them out as a set. - let Ok(pk_set) = gather_sign_node_pks(&state).await else { - tracing::error!("Unable to gather public keys"); - return; + 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; + } }; - let Ok(messages) = broadcast_pk_set(&state, pk_set).await else { - tracing::error!("Unable to broadcast public keys"); - 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"); @@ -463,7 +473,10 @@ async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result results, - Err(err) => return Err(err), + Err(err) => { + tracing::debug!("failed to gather pk: {err}"); + return Err(err); + } }; results.sort_by_key(|(index, _)| *index); @@ -483,11 +496,15 @@ 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", - pk_set, + request, ) .await?; diff --git a/mpc-recovery/src/sign_node/aggregate_signer.rs b/mpc-recovery/src/sign_node/aggregate_signer.rs index 722ed9539..255805996 100644 --- a/mpc-recovery/src/sign_node/aggregate_signer.rs +++ b/mpc-recovery/src/sign_node/aggregate_signer.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use curv::arithmetic::Converter; use curv::cryptographic_primitives::commitments::{ @@ -14,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}; @@ -60,7 +61,7 @@ impl SigningState { Ok(commitment) } - pub fn get_reveal( + pub async fn get_reveal( &mut self, node_info: NodeInfo, recieved_commitments: Vec, @@ -80,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) @@ -167,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(( @@ -240,13 +242,13 @@ impl NodeInfo { } } - fn signed_by_every_node( + async fn signed_by_every_node( &self, signed: Vec, ) -> Result)>, String> { self.nodes_public_keys .read() - .map_err(|e| e.to_string())? + .await .as_ref() .ok_or_else(|| "No nodes public keys available to sign".to_string())? .iter() @@ -332,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, @@ -377,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 3144db5c0..375e7fbd3 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -158,6 +158,7 @@ async fn reveal( .write() .await .get_reveal(state.node_info, request) + .await { Ok(r) => { tracing::debug!("Successful reveal"); @@ -218,14 +219,16 @@ 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<(usize, Point)>) { +) -> (StatusCode, Json), String>>) { ( StatusCode::OK, - Json((state.node_info.our_index, state.node_key.public_key)), + Json(Ok((state.node_info.our_index, state.node_key.public_key))), ) } @@ -233,30 +236,26 @@ async fn public_key_node( async fn accept_pk_set( Extension(state): Extension, Json(request): Json, -) -> (StatusCode, 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(format!( - "Sign node could not accept the public keys: current node index={index} does not match up"))); + return (StatusCode::BAD_REQUEST, Json(Err(format!( + "Sign node could not accept the public keys: current node index={index} does not match up")))); } - let Ok(mut public_keys) = state.node_info.nodes_public_keys.write() else { - return (StatusCode::INTERNAL_SERVER_ERROR, Json("Unable to write into public keys. RwLock potentially poisoned".into())); - }; - *public_keys = Some(request.public_keys); + let mut public_keys = state.node_info.nodes_public_keys.write().await; + tracing::debug!("Setting node public keys => {:?}", request.public_keys); + public_keys.replace(request.public_keys); ( StatusCode::OK, - Json("Successfully set node public keys".to_string()), + Json(Ok("Successfully set node public 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 Ok(public_keys) = state.node_info.nodes_public_keys.read() else { - return Err("Unable to check public keys. RwLock potentially poisoned".into()); - }; - + 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(), From c9da1eacaf2e584f00344a39521b2acba8ef6660 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 20:58:03 +1000 Subject: [PATCH 05/10] write e2e deploy guide for partners (#131) --- DEPLOY.md | 89 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 17 deletions(-) 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. From 79dfea8b719250902877d2fdd48e52336f1ebad7 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 22:02:44 +1000 Subject: [PATCH 06/10] save/load pk_set --- mpc-recovery/src/sign_node/mod.rs | 39 ++++++++++++++--- mpc-recovery/src/sign_node/pk_set.rs | 64 ++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 mpc-recovery/src/sign_node/pk_set.rs diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs index 375e7fbd3..46f479a90 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -4,6 +4,7 @@ use crate::gcp::GcpService; use crate::msg::{AcceptNodePublicKeysRequest, SigShareRequest}; use crate::oauth::{OAuthTokenVerifier, UniversalTokenVerifier}; 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,6 +14,7 @@ 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))] @@ -20,6 +22,11 @@ pub async fn run(gcp_service: GcpService, our_index: NodeId, node_key: ExpandedK tracing::debug!("running a sign node"); let our_index = usize::try_from(our_index).expect("This index is way to big"); + let pk_set = gcp_service + .get::<_, SignerNodePkSet>(pk_set::MAIN_KEY) + .await + .expect("failed to connect to GCP Datastore"); + let pagoda_firebase_audience_id = "pagoda-firebase-audience-id".to_string(); let signing_state = Arc::new(RwLock::new(SigningState::new())); let state = SignNodeState { @@ -27,7 +34,7 @@ pub async fn run(gcp_service: GcpService, our_index: NodeId, node_key: ExpandedK node_key, signing_state, pagoda_firebase_audience_id, - node_info: NodeInfo::new(our_index, None), + node_info: NodeInfo::new(our_index, pk_set.map(|set| set.public_keys)), }; let app = Router::new() @@ -245,12 +252,32 @@ async fn accept_pk_set( } 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); - ( - StatusCode::OK, - Json(Ok("Successfully set node public keys".to_string())), - ) + public_keys.replace(request.public_keys.clone()); + match state + .gcp_service + .insert(SignerNodePkSet { + 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. 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..75fc31bd3 --- /dev/null +++ b/mpc-recovery/src/sign_node/pk_set.rs @@ -0,0 +1,64 @@ +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 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( + "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(MAIN_KEY.to_string()), + id: None, + }]), + partition_id: None, + }, + properties, + } + } +} + +impl FromValue for SignerNodePkSet { + fn from_value(value: Value) -> Result { + match value { + Value::EntityValue { mut properties, .. } => { + 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 { public_keys }) + } + value => Err(ConvertError::UnexpectedPropertyType { + expected: "entity".to_string(), + got: format!("{:?}", value), + }), + } + } +} From 051390b67149ff530dac64b922016e54de82b904 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 22:37:27 +1000 Subject: [PATCH 07/10] limit parallel test execution --- .github/workflows/integration_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index da8e41d5d..15f1cd15c 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -48,4 +48,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 From 5338149aa9f6654639533a4228a3c8e59d985b72 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 22:38:13 +1000 Subject: [PATCH 08/10] reduce the number of nodes for most tests --- integration-tests/tests/mpc/negative.rs | 8 ++++---- integration-tests/tests/mpc/positive.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/integration-tests/tests/mpc/negative.rs b/integration-tests/tests/mpc/negative.rs index 37ee40d55..c73026840 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(); @@ -86,7 +86,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(); @@ -168,7 +168,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(); @@ -252,7 +252,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 f898a5edb..a09bb918b 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(); From 00d6fd5480b2c003f6fede84f6d349a4af01bd0a Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 23:08:58 +1000 Subject: [PATCH 09/10] tweak retry timeouts --- mpc-recovery/src/leader_node/mod.rs | 4 ++-- mpc-recovery/src/sign_node/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 503e91629..2ba14016e 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -463,7 +463,7 @@ async fn add_key( } async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result>> { - let fut = nar::retry_every(std::time::Duration::from_millis(250), || async { + 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, @@ -486,7 +486,7 @@ async fn gather_sign_node_pks(state: &LeaderState) -> anyhow::Result(pk_set::MAIN_KEY) .await - .expect("failed to connect to GCP Datastore"); + .unwrap_or_default(); let pagoda_firebase_audience_id = "pagoda-firebase-audience-id".to_string(); let signing_state = Arc::new(RwLock::new(SigningState::new())); From 134f72c8d06391b6ee3f849b4b4aa96a0e190e45 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Tue, 25 Apr 2023 23:49:26 +1000 Subject: [PATCH 10/10] parametrize pk set by node id --- mpc-recovery/src/sign_node/mod.rs | 3 ++- mpc-recovery/src/sign_node/pk_set.rs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/mpc-recovery/src/sign_node/mod.rs b/mpc-recovery/src/sign_node/mod.rs index 245f0373d..89b98ffec 100644 --- a/mpc-recovery/src/sign_node/mod.rs +++ b/mpc-recovery/src/sign_node/mod.rs @@ -28,7 +28,7 @@ pub async fn run( let our_index = usize::try_from(our_index).expect("This index is way to big"); let pk_set = gcp_service - .get::<_, SignerNodePkSet>(pk_set::MAIN_KEY) + .get::<_, SignerNodePkSet>(format!("{}/{}", our_index, pk_set::MAIN_KEY)) .await .unwrap_or_default(); @@ -280,6 +280,7 @@ async fn accept_pk_set( match state .gcp_service .insert(SignerNodePkSet { + node_id: state.node_info.our_index, public_keys: request.public_keys, }) .await diff --git a/mpc-recovery/src/sign_node/pk_set.rs b/mpc-recovery/src/sign_node/pk_set.rs index 75fc31bd3..457d7addb 100644 --- a/mpc-recovery/src/sign_node/pk_set.rs +++ b/mpc-recovery/src/sign_node/pk_set.rs @@ -12,6 +12,7 @@ pub const MAIN_KEY: &str = "main"; #[derive(Serialize, Deserialize, Clone)] pub struct SignerNodePkSet { + pub node_id: usize, pub public_keys: Vec>, } @@ -24,6 +25,10 @@ impl KeyKind for SignerNodePkSet { 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()), @@ -32,7 +37,7 @@ impl IntoValue for SignerNodePkSet { key: Key { path: Some(vec![PathElement { kind: Some(SignerNodePkSet::kind()), - name: Some(MAIN_KEY.to_string()), + name: Some(format!("{}/{}", self.node_id, MAIN_KEY)), id: None, }]), partition_id: None, @@ -46,6 +51,10 @@ 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()))?; @@ -53,7 +62,10 @@ impl FromValue for SignerNodePkSet { let public_keys = serde_json::from_str(&public_keys) .map_err(|_| ConvertError::MalformedProperty("public_keys".to_string()))?; - Ok(Self { public_keys }) + Ok(Self { + node_id, + public_keys, + }) } value => Err(ConvertError::UnexpectedPropertyType { expected: "entity".to_string(),