diff --git a/Cargo.lock b/Cargo.lock index 6b02e6826..ecaa51d7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1998,6 +1998,7 @@ dependencies = [ "rsa", "serde", "serde_json", + "thiserror", "threshold_crypto", "tokio", "tonic 0.9.1", diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs index 790ee3754..0041270b5 100644 --- a/integration-tests/tests/lib.rs +++ b/integration-tests/tests/lib.rs @@ -1,25 +1,21 @@ +mod docker; +mod mpc; + use crate::docker::{LeaderNode, SignNode}; use bollard::Docker; use docker::{redis::Redis, relayer::Relayer}; use futures::future::BoxFuture; -use mpc_recovery::msg::{ - AddKeyRequest, AddKeyResponse, LeaderRequest, LeaderResponse, NewAccountRequest, - NewAccountResponse, -}; -use rand::{distributions::Alphanumeric, Rng}; use std::time::Duration; use threshold_crypto::PublicKeySet; use workspaces::{network::Sandbox, AccountId, Worker}; -mod docker; - const NETWORK: &str = "mpc_recovery_integration_test_network"; #[cfg(target_os = "linux")] const HOST_MACHINE_FROM_DOCKER: &str = "172.17.0.1"; #[cfg(target_os = "macos")] const HOST_MACHINE_FROM_DOCKER: &str = "docker.for.mac.localhost"; -struct TestContext<'a> { +pub struct TestContext<'a> { leader_node: &'a LeaderNode, pk_set: &'a PublicKeySet, worker: &'a Worker, @@ -117,108 +113,96 @@ where result } -#[tokio::test] -async fn test_trio() -> anyhow::Result<()> { - with_nodes(4, 3, 3, |ctx| { - Box::pin(async move { - let payload: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(10) - .map(char::from) - .collect(); - let (status_code, response) = ctx - .leader_node - .submit(LeaderRequest { - payload: payload.clone(), - }) - .await?; - - assert_eq!(status_code, 200); - if let LeaderResponse::Ok { signature } = response { - assert!(ctx.pk_set.public_key().verify(&signature, payload)); - } else { - panic!("response was not successful"); - } +mod account { + use rand::{distributions::Alphanumeric, Rng}; + use workspaces::{network::Sandbox, AccountId, Worker}; + + pub fn random(worker: &Worker) -> anyhow::Result { + let account_id_rand: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + Ok(format!( + "mpc-recovery-{}.{}", + account_id_rand.to_lowercase(), + worker.root_account()?.id() + ) + .parse()?) + } - Ok(()) - }) - }) - .await + pub fn malformed() -> String { + let random: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + format!("malformed-account-{}-!@#$%", random.to_lowercase()) + } } -#[tokio::test] -async fn test_basic_action() -> anyhow::Result<()> { - with_nodes(4, 3, 3, |ctx| { - Box::pin(async move { - // Create new account - // TODO: write a test with real token - // "validToken" should triger test token verifyer and return success - let id_token = "validToken".to_string(); - let account_id_rand: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(10) - .map(char::from) - .collect(); - let account_id: AccountId = format!( - "mpc-recovery-{}.{}", - account_id_rand.to_lowercase(), - ctx.worker.root_account()?.id() - ) - .parse() - .unwrap(); - - let user_public_key = - near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519) - .public_key() - .to_string(); - - let (status_code, new_acc_response) = ctx - .leader_node - .new_account(NewAccountRequest { - near_account_id: account_id.to_string(), - oidc_token: id_token.clone(), - public_key: user_public_key.clone(), - }) - .await - .unwrap(); - assert_eq!(status_code, 200); - assert!(matches!(new_acc_response, NewAccountResponse::Ok)); - - tokio::time::sleep(Duration::from_millis(2000)).await; - - // Check that account exists and it has the requested public key - let access_keys = ctx.worker.view_access_keys(&account_id).await?; - assert!(access_keys - .iter() - .any(|ak| ak.public_key.to_string() == user_public_key)); - - let new_user_public_key = - near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519) - .public_key() - .to_string(); - - let (status_code2, add_key_response) = ctx - .leader_node - .add_key(AddKeyRequest { - near_account_id: account_id.to_string(), - oidc_token: id_token.clone(), - public_key: new_user_public_key.clone(), - }) - .await?; - - assert_eq!(status_code2, 200); - assert!(matches!(add_key_response, AddKeyResponse::Ok)); - - tokio::time::sleep(Duration::from_millis(2000)).await; - - // Check that account has the requested public key - let access_keys = ctx.worker.view_access_keys(&account_id).await?; - assert!(access_keys - .iter() - .any(|ak| ak.public_key.to_string() == new_user_public_key)); +mod key { + use rand::{distributions::Alphanumeric, Rng}; + + pub fn random() -> String { + near_crypto::SecretKey::from_random(near_crypto::KeyType::ED25519) + .public_key() + .to_string() + } + pub fn malformed() -> String { + let random: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + format!("malformed-key-{}-!@#$%", random.to_lowercase()) + } +} + +mod token { + pub fn valid() -> String { + "validToken".to_string() + } + + pub fn invalid() -> String { + "invalidToken".to_string() + } +} + +mod check { + use crate::TestContext; + use workspaces::AccountId; + + pub async fn access_key_exists<'a>( + ctx: &TestContext<'a>, + account_id: &AccountId, + public_key: &str, + ) -> anyhow::Result<()> { + let access_keys = ctx.worker.view_access_keys(account_id).await?; + + if access_keys + .iter() + .any(|ak| ak.public_key.to_string() == public_key) + { Ok(()) - }) - }) - .await + } else { + Err(anyhow::anyhow!( + "could not find access key {public_key} on account {account_id}" + )) + } + } + + pub async fn no_account<'a>( + ctx: &TestContext<'a>, + account_id: &AccountId, + ) -> anyhow::Result<()> { + if ctx.worker.view_account(account_id).await.is_err() { + Ok(()) + } else { + Err(anyhow::anyhow!( + "expected account {account_id} to not exist, but it does" + )) + } + } } diff --git a/integration-tests/tests/mpc/mod.rs b/integration-tests/tests/mpc/mod.rs new file mode 100644 index 000000000..742ad6588 --- /dev/null +++ b/integration-tests/tests/mpc/mod.rs @@ -0,0 +1,2 @@ +mod negative; +mod positive; diff --git a/integration-tests/tests/mpc/negative.rs b/integration-tests/tests/mpc/negative.rs new file mode 100644 index 000000000..793edb1fb --- /dev/null +++ b/integration-tests/tests/mpc/negative.rs @@ -0,0 +1,243 @@ +use crate::{account, check, key, token, with_nodes}; +use mpc_recovery::msg::{AddKeyRequest, AddKeyResponse, NewAccountRequest, NewAccountResponse}; +use std::time::Duration; + +#[tokio::test] +async fn test_invalid_token() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let account_id = account::random(ctx.worker)?; + let user_public_key = key::random(); + + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::invalid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 401); + assert!(matches!(new_acc_response, NewAccountResponse::Err { .. })); + + // Check that the service is still available + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(new_acc_response, NewAccountResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &user_public_key).await?; + + let new_user_public_key = key::random(); + + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::invalid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 401); + assert!(matches!(add_key_response, AddKeyResponse::Err { .. })); + + // Check that the service is still available + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(add_key_response, AddKeyResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &new_user_public_key).await?; + + Ok(()) + }) + }) + .await +} + +#[tokio::test] +async fn test_malformed_account_id() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let malformed_account_id = account::malformed(); + let user_public_key = key::random(); + + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: malformed_account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 500); + assert!(matches!(new_acc_response, NewAccountResponse::Err { .. })); + + let account_id = account::random(ctx.worker)?; + + // Check that the service is still available + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(new_acc_response, NewAccountResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &user_public_key).await?; + + let new_user_public_key = key::random(); + + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: malformed_account_id.to_string(), + oidc_token: token::valid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 500); + assert!(matches!(add_key_response, AddKeyResponse::Err { .. })); + + // Check that the service is still available + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(add_key_response, AddKeyResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &new_user_public_key).await?; + + Ok(()) + }) + }) + .await +} + +#[tokio::test] +async fn test_malformed_public_key() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let account_id = account::random(ctx.worker)?; + let malformed_public_key = key::malformed(); + + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: malformed_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 500); + assert!(matches!(new_acc_response, NewAccountResponse::Err { .. })); + + let user_public_key = key::random(); + + // Check that the service is still available + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(new_acc_response, NewAccountResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &user_public_key).await?; + + let new_user_public_key = key::random(); + + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: malformed_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 500); + assert!(matches!(add_key_response, AddKeyResponse::Err { .. })); + + // Check that the service is still available + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(add_key_response, AddKeyResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &new_user_public_key).await?; + + Ok(()) + }) + }) + .await +} + +#[tokio::test] +async fn test_add_key_to_non_existing_account() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let account_id = account::random(ctx.worker)?; + let user_public_key = key::random(); + + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + + assert_eq!(status_code, 400); + assert!(matches!(add_key_response, AddKeyResponse::Err { .. })); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::no_account(&ctx, &account_id).await?; + + Ok(()) + }) + }) + .await +} diff --git a/integration-tests/tests/mpc/positive.rs b/integration-tests/tests/mpc/positive.rs new file mode 100644 index 000000000..1bf15af08 --- /dev/null +++ b/integration-tests/tests/mpc/positive.rs @@ -0,0 +1,82 @@ +use crate::{account, check, key, token, with_nodes}; +use mpc_recovery::msg::{ + AddKeyRequest, AddKeyResponse, LeaderRequest, LeaderResponse, NewAccountRequest, + NewAccountResponse, +}; +use rand::{distributions::Alphanumeric, Rng}; +use std::time::Duration; + +#[tokio::test] +async fn test_trio() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let payload: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + let (status_code, response) = ctx + .leader_node + .submit(LeaderRequest { + payload: payload.clone(), + }) + .await?; + + assert_eq!(status_code, 200); + if let LeaderResponse::Ok { signature } = response { + assert!(ctx.pk_set.public_key().verify(&signature, payload)); + } else { + panic!("response was not successful"); + } + + Ok(()) + }) + }) + .await +} + +// TODO: write a test with real token +#[tokio::test] +async fn test_basic_action() -> anyhow::Result<()> { + with_nodes(4, 3, 3, |ctx| { + Box::pin(async move { + let account_id = account::random(ctx.worker)?; + let user_public_key = key::random(); + + let (status_code, new_acc_response) = ctx + .leader_node + .new_account(NewAccountRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(new_acc_response, NewAccountResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &user_public_key).await?; + + let new_user_public_key = key::random(); + + let (status_code, add_key_response) = ctx + .leader_node + .add_key(AddKeyRequest { + near_account_id: account_id.to_string(), + oidc_token: token::valid(), + public_key: new_user_public_key.clone(), + }) + .await?; + assert_eq!(status_code, 200); + assert!(matches!(add_key_response, AddKeyResponse::Ok)); + + tokio::time::sleep(Duration::from_millis(2000)).await; + + check::access_key_exists(&ctx, &account_id, &new_user_public_key).await?; + + Ok(()) + }) + }) + .await +} diff --git a/mpc-recovery/Cargo.toml b/mpc-recovery/Cargo.toml index 0b1e74907..7aa199db2 100644 --- a/mpc-recovery/Cargo.toml +++ b/mpc-recovery/Cargo.toml @@ -37,6 +37,7 @@ reqwest = "0.11.16" rsa = "0.8.2" serde = { version = "1", features = ["derive"] } serde_json = "1" +thiserror = "1" threshold_crypto = "0.4.0" tokio = { version = "1.0", features = ["full"] } tonic = { version = "0.9", features = ["tls"] } diff --git a/mpc-recovery/src/leader_node/mod.rs b/mpc-recovery/src/leader_node/mod.rs index 147f44fed..deb1aaa00 100644 --- a/mpc-recovery/src/leader_node/mod.rs +++ b/mpc-recovery/src/leader_node/mod.rs @@ -5,6 +5,7 @@ use crate::msg::{ }; use crate::oauth::{IdTokenClaims, OAuthTokenVerifier, UniversalTokenVerifier}; use crate::primitives::InternalAccountId; +use crate::relayer::error::RelayerError; use crate::relayer::msg::RegisterAccountRequest; use crate::relayer::NearRpcAndRelayerClient; use crate::transaction::{ @@ -235,13 +236,33 @@ async fn process_add_key( let user_account_id: AccountId = request.near_account_id.parse()?; // Get nonce and recent block hash - let nonce = state + let nonce = match state .client .access_key_nonce( user_account_id.clone(), get_user_recovery_pk(internal_acc_id.clone()).clone(), ) - .await?; + .await + { + Ok(nonce) => nonce, + Err(RelayerError::UnknownAccount(account_id)) => { + return Ok(( + StatusCode::BAD_REQUEST, + Json(AddKeyResponse::Err { + msg: format!("account {account_id} does not exist"), + }), + )) + } + Err(RelayerError::UnknownAccessKey(public_key)) => { + return Ok(( + StatusCode::BAD_REQUEST, + Json(AddKeyResponse::Err { + msg: format!("public key {public_key} does not exist"), + }), + )) + } + Err(RelayerError::Other(e)) => return Err(e), + }; let block_height = state.client.latest_block_height().await?; // Create a transaction to create a new account diff --git a/mpc-recovery/src/relayer/error.rs b/mpc-recovery/src/relayer/error.rs new file mode 100644 index 000000000..e0a39b262 --- /dev/null +++ b/mpc-recovery/src/relayer/error.rs @@ -0,0 +1,12 @@ +use near_crypto::PublicKey; +use near_primitives::types::AccountId; + +#[derive(thiserror::Error, Debug)] +pub enum RelayerError { + #[error("unknown account `{0}`")] + UnknownAccount(AccountId), + #[error("unknown key `{0}`")] + UnknownAccessKey(PublicKey), + #[error("{0}")] + Other(#[from] anyhow::Error), +} diff --git a/mpc-recovery/src/relayer/mod.rs b/mpc-recovery/src/relayer/mod.rs index e59caea27..8575cf71b 100644 --- a/mpc-recovery/src/relayer/mod.rs +++ b/mpc-recovery/src/relayer/mod.rs @@ -1,12 +1,16 @@ +pub mod error; pub mod msg; use hyper::{Body, Client, Method, Request}; +use near_jsonrpc_client::errors::{JsonRpcError, JsonRpcServerError}; +use near_jsonrpc_client::methods::query::RpcQueryError; use near_jsonrpc_client::{methods, JsonRpcClient}; use near_jsonrpc_primitives::types::query::QueryResponseKind; use near_primitives::hash::CryptoHash; use near_primitives::types::{AccountId, BlockHeight, Finality}; use near_primitives::views::{AccessKeyView, QueryRequest}; +use self::error::RelayerError; use self::msg::{RegisterAccountRequest, SendMetaTxRequest, SendMetaTxResponse}; #[derive(Clone)] @@ -27,7 +31,7 @@ impl NearRpcAndRelayerClient { &self, account_id: AccountId, public_key: near_crypto::PublicKey, - ) -> anyhow::Result<(AccessKeyView, CryptoHash)> { + ) -> Result<(AccessKeyView, CryptoHash), RelayerError> { let query_resp = self .rpc_client .call(&methods::query::RpcQueryRequest { @@ -38,13 +42,24 @@ impl NearRpcAndRelayerClient { }, }) .await - .map_err(|e| anyhow::anyhow!("failed to query access key {}", e))?; + .map_err(|e| match e { + JsonRpcError::ServerError(JsonRpcServerError::HandlerError( + RpcQueryError::UnknownAccount { + requested_account_id, + .. + }, + )) => RelayerError::UnknownAccount(requested_account_id), + JsonRpcError::ServerError(JsonRpcServerError::HandlerError( + RpcQueryError::UnknownAccessKey { public_key, .. }, + )) => RelayerError::UnknownAccessKey(public_key), + _ => anyhow::anyhow!(e).into(), + })?; match query_resp.kind { QueryResponseKind::AccessKey(access_key) => Ok((access_key, query_resp.block_hash)), - _ => Err(anyhow::anyhow!( - "query returned invalid data while querying access key" - )), + _ => { + Err(anyhow::anyhow!("query returned invalid data while querying access key").into()) + } } } @@ -52,7 +67,7 @@ impl NearRpcAndRelayerClient { &self, account_id: AccountId, public_key: near_crypto::PublicKey, - ) -> anyhow::Result { + ) -> Result { let key = self.access_key(account_id, public_key).await?; Ok(key.0.nonce) }