diff --git a/kairos-cli/src/client.rs b/kairos-cli/src/client.rs index b1c68d4a..3c0d5f3c 100644 --- a/kairos-cli/src/client.rs +++ b/kairos-cli/src/client.rs @@ -3,6 +3,8 @@ use casper_client::types::{DeployBuilder, DeployHash, ExecutableDeployItem, Time use casper_client_types::{crypto::SecretKey, runtime_args, ContractHash, RuntimeArgs, U512}; use kairos_server::routes::contract_hash::ContractHashPath; use kairos_server::routes::deposit::DepositPath; +use kairos_server::routes::get_nonce::GetNoncePath; +use kairos_server::PublicKey; use reqwest::Url; use serde::{Deserialize, Serialize}; use std::fmt; @@ -18,7 +20,7 @@ pub enum KairosClientError { ResponseErrorWithCode(u16, String), DecodeError(String), CasperClientError(String), - KairosServerError(String), + KairosServerError(u16, String), } impl std::error::Error for KairosClientError {} @@ -84,15 +86,29 @@ pub fn deposit( .header("Content-Type", "application/json") .json(&deploy) .send() - .map_err(KairosClientError::from)?; + .map_err(KairosClientError::from)? + .error_for_status(); - let status = response.status(); - if !status.is_success() { - Err(KairosClientError::KairosServerError(status.to_string())) - } else { - response + match response { + Err(err) => Err(KairosClientError::from(err)), + Ok(response) => response .json::() - .map_err(KairosClientError::from) + .map_err(KairosClientError::from), + } +} + +pub fn get_nonce(base_url: &Url, account: &PublicKey) -> Result { + let response = reqwest::blocking::Client::new() + .post(base_url.join(GetNoncePath::PATH).unwrap()) + .header("Content-Type", "application/json") + .json(&account) + .send() + .map_err(KairosClientError::from)? + .error_for_status(); + + match response { + Err(err) => Err(KairosClientError::from(err)), + Ok(response) => response.json::().map_err(KairosClientError::from), } } @@ -105,7 +121,10 @@ pub fn contract_hash(base_url: &Url) -> Result let status = response.status(); if !status.is_success() { - Err(KairosClientError::KairosServerError(status.to_string())) + Err(KairosClientError::KairosServerError( + status.as_u16(), + status.to_string(), + )) } else { response .json::() diff --git a/kairos-cli/src/commands/transfer.rs b/kairos-cli/src/commands/transfer.rs index 7da26d15..1756429a 100644 --- a/kairos-cli/src/commands/transfer.rs +++ b/kairos-cli/src/commands/transfer.rs @@ -1,4 +1,4 @@ -use crate::client::KairosClientError; +use crate::client::{self, KairosClientError}; use crate::common::args::{AmountArg, NonceArg, PrivateKeyPathArg, RecipientArg}; use crate::error::CliError; @@ -30,7 +30,11 @@ pub fn run(args: Args, kairos_server_address: Url) -> Result { let amount: u64 = args.amount.field; let signer = Signer::from_private_key_file(args.private_key_path.field).map_err(CryptoError::from)?; - let nonce = args.nonce.val; + let signer_public_key = signer.to_public_key()?; + let nonce = match args.nonce.val { + None => client::get_nonce(&kairos_server_address, &signer_public_key)?, + Some(nonce) => nonce, + }; // TODO: Create transaction and sign it with `signer`. @@ -38,7 +42,7 @@ pub fn run(args: Args, kairos_server_address: Url) -> Result { let res = reqwest::blocking::Client::new() .post(kairos_server_address.join(TransferPath::PATH).unwrap()) .json(&PayloadBody { - public_key: signer.to_public_key()?, + public_key: signer_public_key, payload: SigningPayload::new(nonce, Transfer::new(recipient, amount)) .try_into() .unwrap(), diff --git a/kairos-cli/src/commands/withdraw.rs b/kairos-cli/src/commands/withdraw.rs index 9ece37a7..031817f4 100644 --- a/kairos-cli/src/commands/withdraw.rs +++ b/kairos-cli/src/commands/withdraw.rs @@ -1,4 +1,4 @@ -use crate::client::KairosClientError; +use crate::client::{self, KairosClientError}; use crate::common::args::{AmountArg, NonceArg, PrivateKeyPathArg}; use crate::error::CliError; @@ -27,7 +27,11 @@ pub fn run(args: Args, kairos_server_address: Url) -> Result { let amount: u64 = args.amount.field; let signer = Signer::from_private_key_file(args.private_key_path.field).map_err(CryptoError::from)?; - let nonce = args.nonce.val; + let signer_public_key = signer.to_public_key()?; + let nonce = match args.nonce.val { + None => client::get_nonce(&kairos_server_address, &signer_public_key)?, + Some(nonce) => nonce, + }; // TODO: Create transaction and sign it with `signer`. @@ -35,7 +39,7 @@ pub fn run(args: Args, kairos_server_address: Url) -> Result { let res = reqwest::blocking::Client::new() .post(kairos_server_address.join(WithdrawPath::PATH).unwrap()) .json(&PayloadBody { - public_key: signer.to_public_key()?, + public_key: signer_public_key, payload: SigningPayload::new(nonce, Withdrawal::new(amount)) .try_into() .unwrap(), diff --git a/kairos-cli/src/common/args.rs b/kairos-cli/src/common/args.rs index 80e6375e..50cc5bf5 100644 --- a/kairos-cli/src/common/args.rs +++ b/kairos-cli/src/common/args.rs @@ -21,7 +21,7 @@ pub struct PrivateKeyPathArg { #[derive(Args, Debug)] pub struct NonceArg { #[arg(id = "nonce", long, short, value_name = "NUM")] - pub val: u64, + pub val: Option, } #[derive(Args, Debug)] diff --git a/kairos-cli/tests/cli_tests.rs b/kairos-cli/tests/cli_tests.rs index 8e9fb123..2678e3db 100644 --- a/kairos-cli/tests/cli_tests.rs +++ b/kairos-cli/tests/cli_tests.rs @@ -101,8 +101,6 @@ fn transfer_successful_with_secp256k1() { .arg(recipient) .arg("--amount") .arg("123") - .arg("--nonce") - .arg("0") .arg("--private-key") .arg(secret_key_path); @@ -120,8 +118,6 @@ fn withdraw_successful_with_ed25519() { cmd.arg("withdraw") .arg("--amount") .arg("123") - .arg("--nonce") - .arg("0") .arg("--private-key") .arg(secret_key_path); @@ -197,9 +193,7 @@ fn transfer_invalid_recipient() { .arg("--amount") .arg("123") .arg("--private-key") - .arg(secret_key_path) - .arg("--nonce") - .arg("0"); + .arg(secret_key_path); cmd.assert() .failure() @@ -218,9 +212,7 @@ fn transfer_valid_recipient() { .arg("--amount") .arg("123") .arg("--private-key") - .arg(secret_key_path) - .arg("--nonce") - .arg("0"); + .arg(secret_key_path); cmd.assert() .failure() diff --git a/kairos-prover/kairos-circuit-logic/src/account_trie.rs b/kairos-prover/kairos-circuit-logic/src/account_trie.rs index cbb1bcce..4b3e99f3 100644 --- a/kairos-prover/kairos-circuit-logic/src/account_trie.rs +++ b/kairos-prover/kairos-circuit-logic/src/account_trie.rs @@ -310,6 +310,16 @@ impl> AccountTrie> { Ok(()) } } + + /// Returns the nonce for an accounts public key if it's known, returns an error if unknown. + pub fn get_nonce_for(&self, account: &PublicKey) -> Result { + let [account_hash] = hash_buffers([account]); + + let account = self.txn.get_exclude_from_txn(&account_hash)?; + let account = account.ok_or("Unknown account")?; + + Ok(account.nonce) + } } /// An account in the trie. diff --git a/kairos-server/src/lib.rs b/kairos-server/src/lib.rs index 8c2493cc..a83af74f 100644 --- a/kairos-server/src/lib.rs +++ b/kairos-server/src/lib.rs @@ -20,7 +20,7 @@ use crate::state::{BatchStateManager, ServerState, ServerStateInner}; pub use errors::AppErr; /// TODO: support secp256k1 -type PublicKey = Vec; +pub type PublicKey = Vec; type Signature = Vec; #[cfg(not(feature = "deposit-mock"))] @@ -39,6 +39,7 @@ pub fn app_router(state: ServerState) -> Router { .typed_post(routes::withdraw_handler) .typed_post(routes::transfer_handler) .typed_post(routes::deposit_mock_handler) + .typed_post(routes::get_nonce_handler) .typed_get(routes::contract_hash_handler) .with_state(state) } diff --git a/kairos-server/src/routes/get_nonce.rs b/kairos-server/src/routes/get_nonce.rs new file mode 100644 index 00000000..42177afe --- /dev/null +++ b/kairos-server/src/routes/get_nonce.rs @@ -0,0 +1,19 @@ +use axum::{extract::State, Json}; +use axum_extra::routing::TypedPath; +use tracing::*; + +use crate::{state::ServerState, AppErr, PublicKey}; + +#[derive(TypedPath, Debug, Clone, Copy)] +#[typed_path("/api/v1/nonce")] +pub struct GetNoncePath; + +#[instrument(level = "trace", skip(state), ret)] +pub async fn get_nonce_handler( + _: GetNoncePath, + state: State, + Json(body): Json, +) -> Result, AppErr> { + let nonce = state.batch_state_manager.get_nonce_for(body).await?; + Ok(Json(nonce)) +} diff --git a/kairos-server/src/routes/mod.rs b/kairos-server/src/routes/mod.rs index 689108c0..b80a206e 100644 --- a/kairos-server/src/routes/mod.rs +++ b/kairos-server/src/routes/mod.rs @@ -2,6 +2,7 @@ pub mod contract_hash; pub mod deposit; #[cfg(feature = "deposit-mock")] pub mod deposit_mock; +pub mod get_nonce; pub mod transfer; pub mod withdraw; @@ -9,6 +10,7 @@ pub use contract_hash::contract_hash_handler; pub use deposit::deposit_handler; #[cfg(feature = "deposit-mock")] pub use deposit_mock::deposit_mock_handler; +pub use get_nonce::get_nonce_handler; pub use transfer::transfer_handler; pub use withdraw::withdraw_handler; diff --git a/kairos-server/src/state.rs b/kairos-server/src/state.rs index dddd3f3c..a12e94cf 100644 --- a/kairos-server/src/state.rs +++ b/kairos-server/src/state.rs @@ -13,7 +13,7 @@ use tokio::{ use casper_client::types::DeployHash; pub use self::trie::TrieStateThreadMsg; -use crate::{config::ServerConfig, state::submit_batch::submit_proof_to_contract}; +use crate::{config::ServerConfig, state::submit_batch::submit_proof_to_contract, PublicKey}; use kairos_circuit_logic::transactions::KairosTransaction; use kairos_trie::{stored::memory_db::MemoryDb, NodeHash, TrieRoot}; @@ -138,4 +138,21 @@ impl BatchStateManager { .await .expect("Never received response from trie thread") } + + pub async fn get_nonce_for(&self, account: PublicKey) -> Result { + let (msg, response) = TrieStateThreadMsg::get_nonce_for(account); + + self.queued_transactions.send(msg).await.map_err(|err| { + tracing::error!("Could not send get-nonce request to trie thread {:?}", err); + crate::AppErr::new(err) + })?; + + response.await.map_err(|err| { + tracing::error!( + "Never received response from the trie thread for the get-nonce-request {:?}", + err + ); + crate::AppErr::new(err) + })? + } } diff --git a/kairos-server/src/state/trie.rs b/kairos-server/src/state/trie.rs index 2e2d117e..0e269444 100644 --- a/kairos-server/src/state/trie.rs +++ b/kairos-server/src/state/trie.rs @@ -20,12 +20,15 @@ use kairos_trie::{ DigestHasher, NodeHash, TrieRoot, }; +use kairos_circuit_logic::transactions::PublicKey; + pub type Database = MemoryDb; #[derive(Debug)] pub enum TrieStateThreadMsg { Transaction(KairosTransaction, oneshot::Sender>), Commit(oneshot::Sender>), + GetNonce(PublicKey, oneshot::Sender>), } impl TrieStateThreadMsg { @@ -38,6 +41,11 @@ impl TrieStateThreadMsg { let (sender, receiver) = oneshot::channel(); (Self::Commit(sender), receiver) } + + pub fn get_nonce_for(account: PublicKey) -> (Self, oneshot::Receiver>) { + let (sender, receiver) = oneshot::channel(); + (Self::GetNonce(account, sender), receiver) + } } pub fn spawn_state_thread( @@ -103,6 +111,23 @@ pub fn spawn_state_thread( tracing::error!("failed to send commit result: {:?}", err); } } + TrieStateThreadMsg::GetNonce(account, responder) => { + let res = state + .batch_state + .account_trie + .get_nonce_for(&account) + .map_err(|err| { + AppErr::new(anyhow::anyhow!(err)) + .set_status(axum::http::StatusCode::NOT_FOUND) + }); + if let Err(err) = responder.send(res) { + tracing::error!( + "Failed to get the nonce for account '{:?}': {:?}", + account, + err + ); + } + } } } }) diff --git a/nixos/tests/end-to-end.nix b/nixos/tests/end-to-end.nix index 97866f47..edaa8443 100644 --- a/nixos/tests/end-to-end.nix +++ b/nixos/tests/end-to-end.nix @@ -18,6 +18,7 @@ let contractHashName = "kairos_contract_package_hash"; # The path where cctl will write the deployed contract hash on the servers filesystem serverContractHashPath = "${cctlWorkingDirectory}/contracts/${contractHashName}"; + casperSyncInterval = 5; in nixosTest { name = "kairos e2e test"; @@ -57,7 +58,7 @@ nixosTest { services.kairos = { casperRpcUrl = "http://localhost:${builtins.toString config.services.cctl.port}/rpc"; casperSseUrl = "http://localhost:18101/events/main"; # has to be hardcoded since it's not configurable atm - casperSyncInterval = 5; + inherit casperSyncInterval; demoContractHash = "0000000000000000000000000000000000000000000000000000000000000000"; }; @@ -131,13 +132,12 @@ nixosTest { wait_for_successful_deploy(deposit_deploy_hash) - # wait for l2 to sync with l1 every 10 seconds - time.sleep(12) - + # wait for l2 to sync with l1 every 5 seconds + time.sleep(${builtins.toString (casperSyncInterval * 2)}) # transfer beneficiary = client.succeed("cat ${clientUsersDirectory}/user-3/public_key_hex") - transfer_output = client.succeed("kairos-cli --kairos-server-address http://kairos transfer --nonce 0 --amount 1000 --recipient {} --private-key {}".format(beneficiary, depositor_private_key)) + transfer_output = client.succeed("kairos-cli --kairos-server-address http://kairos transfer --amount 1000 --recipient {} --private-key {}".format(beneficiary, depositor_private_key)) assert "Transfer successfully sent to L2\n" in transfer_output, "The transfer command was not successful: {}".format(transfer_output) # TODO test withdraw