From 7203fe9a63dca901898af216e44751c48992d38d Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 11 Jul 2024 12:27:20 -0400 Subject: [PATCH] feat: add `ledgers-from-now` arg to invoke and auth This allows users to specify how long a signed auth entry is valid. --- FULL_HELP_DOCS.md | 6 ++ .../soroban-test/tests/it/integration/tx.rs | 55 +++++++++++++++++++ cmd/soroban-cli/src/commands/config/mod.rs | 10 +++- .../src/commands/contract/invoke.rs | 4 +- cmd/soroban-cli/src/commands/tx/auth.rs | 19 +++++++ cmd/soroban-cli/src/commands/tx/mod.rs | 1 + cmd/soroban-cli/src/commands/tx/sign.rs | 4 +- cmd/soroban-cli/src/signer.rs | 13 ++--- 8 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 cmd/soroban-cli/src/commands/tx/auth.rs diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 9c566a967a..d6281c433b 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -522,6 +522,9 @@ stellar contract invoke ... -- --help * `--instructions ` — Number of instructions to simulate * `--build-only` — Build the transaction and only write the base64 xdr to stdout * `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--ledgers-from-now ` — Number of ledgers from current ledger before the signed auth entry expires. Default 60 ~ 5 minutes + + Default value: `60` @@ -1256,6 +1259,9 @@ Sign a transaction * `--global` — Use global config * `--config-dir ` — Location of config directory, default is "." * `--check` — Check with user before signature. Eventually this will be replaced with `--yes`, which does the opposite and will force a check without --yes +* `--ledgers-from-now ` — Number of ledgers from current ledger before the signed auth entry expires. Default 60 ~ 5 minutes + + Default value: `60` * `-a`, `--auth-only` — Only sign the Authorization Entries required by the provided source account diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index ce48cc8aeb..9c568f8e97 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -128,3 +128,58 @@ async fn sign() { .success() .stdout(predicates::str::contains(r#""status": "SUCCESS""#)); } + +#[tokio::test] +async fn expired_auth_entry() { + let sandbox = &TestEnv::new(); + let id = &deploy_hello(sandbox).await; + // Create new test_other account + sandbox + .new_assert_cmd("keys") + .arg("generate") + .arg("test_other") + .assert(); + + // Get Xdr for transaction where auth is required for test_other + let xdr_base64 = sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--sim-only") + .arg("--") + .arg("auth") + .arg("--world=world") + .arg("--addr=test_other") + .assert() + .success() + .stdout_as_str(); + // Sign the transaction's auth entry with test_other + let xdr_base64 = sandbox + .new_assert_cmd("tx") + .arg("sign") + .arg("--auth") + .arg("--source=test_other") + .arg("--expiration-ledger=1") + .write_stdin(xdr_base64.as_bytes()) + .assert() + .success() + .stdout_as_str(); + tokio::sleep(tokio::time::Duration::from_secs(10)).await; + // Sign the transaction with test as source account + let xdr_base64 = sandbox + .new_assert_cmd("tx") + .arg("sign") + .write_stdin(xdr_base64.as_bytes()) + .assert() + .success() + .stdout_as_str(); + // Send transaction + sandbox + .new_assert_cmd("tx") + .arg("send") + .write_stdin(xdr_base64.as_bytes()) + .assert() + .stdout(predicates::str::contains(r#""status": "FAILED""#)) + .stdout(predicates::str::contains("signature has expired")); +} diff --git a/cmd/soroban-cli/src/commands/config/mod.rs b/cmd/soroban-cli/src/commands/config/mod.rs index 630d0a343e..377385d373 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/commands/config/mod.rs @@ -90,17 +90,23 @@ impl Args { pub async fn sign_soroban_authorizations( &self, tx: &Transaction, + ledgers_from_current: u32, ) -> Result, Error> { - self.sign_soroban_authorizations_with_signer(&self.signer()?, tx) + self.sign_soroban_authorizations_with_signer(&self.signer()?, tx, ledgers_from_current) .await } pub async fn sign_soroban_authorizations_with_signer( &self, signer: &(impl Stellar + std::marker::Sync), tx: &Transaction, + ledgers_from_current: u32, ) -> Result, Error> { let network = self.get_network()?; - Ok(signer.sign_soroban_authorizations(tx, &network).await?) + let client = crate::rpc::Client::new(&network.rpc_url)?; + let expiration_ledger = client.get_latest_ledger().await?.sequence + ledgers_from_current; + Ok(signer + .sign_soroban_authorizations(tx, &network, expiration_ledger) + .await?) } pub fn get_network(&self) -> Result { diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index de9c94aed8..7bfe84b7a8 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -59,6 +59,8 @@ pub struct Cmd { pub config: config::Args, #[command(flatten)] pub fee: crate::fee::Args, + #[command(flatten)] + pub ledgers: crate::commands::tx::auth::Args, } impl FromStr for Cmd { @@ -393,7 +395,7 @@ impl NetworkRunnable for Cmd { // crate::log::auth(&[auth]); for signer in &signers { if let Some(tx) = config - .sign_soroban_authorizations_with_signer(signer, &txn) + .sign_soroban_authorizations_with_signer(signer, &txn, self.ledgers.from_now) .await? { txn = tx; diff --git a/cmd/soroban-cli/src/commands/tx/auth.rs b/cmd/soroban-cli/src/commands/tx/auth.rs new file mode 100644 index 0000000000..cf91c29915 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/auth.rs @@ -0,0 +1,19 @@ +use clap::arg; + +#[derive(Debug, clap::Args, Clone)] +#[group(skip)] +pub struct Args { + /// Number of ledgers from current ledger before the signed auth entry expires. Default 60 ~ 5 minutes. + #[arg( + long = "ledgers-from-now", + visible_alias = "ledgers", + default_value = "60" + )] + pub from_now: u32, +} + +impl Default for Args { + fn default() -> Self { + Self { from_now: 60 } + } +} diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 8de6c8c19f..2ab513b6bf 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -2,6 +2,7 @@ use clap::Parser; use super::global; +pub mod auth; pub mod send; pub mod sign; pub mod simulate; diff --git a/cmd/soroban-cli/src/commands/tx/sign.rs b/cmd/soroban-cli/src/commands/tx/sign.rs index 5fb48d2b57..e4e66e4f2a 100644 --- a/cmd/soroban-cli/src/commands/tx/sign.rs +++ b/cmd/soroban-cli/src/commands/tx/sign.rs @@ -17,6 +17,8 @@ pub enum Error { pub struct Cmd { #[clap(flatten)] pub config: config::Args, + #[clap(flatten)] + pub ledgers: super::auth::Args, /// Only sign the Authorization Entries required by the provided source account #[arg(long, visible_alias = "auth", short = 'a')] pub auth_only: bool, @@ -35,7 +37,7 @@ impl Cmd { if self.auth_only { Ok(self .config - .sign_soroban_authorizations(&tx) + .sign_soroban_authorizations(&tx, self.ledgers.from_now) .await? .unwrap_or(tx) .into()) diff --git a/cmd/soroban-cli/src/signer.rs b/cmd/soroban-cli/src/signer.rs index f78ac707cb..7213de3afa 100644 --- a/cmd/soroban-cli/src/signer.rs +++ b/cmd/soroban-cli/src/signer.rs @@ -103,6 +103,7 @@ pub trait Stellar { &self, raw: &Transaction, network: &Network, + expiration_ledger: u32, ) -> Result, Error> { let mut tx = raw.clone(); let Some(mut op) = requires_auth(&tx) else { @@ -116,12 +117,10 @@ pub trait Stellar { else { return Ok(None); }; - let client = crate::rpc::Client::new(&network.rpc_url)?; let mut auths = body.auth.to_vec(); - let current_ledger = client.get_latest_ledger().await?.sequence; for auth in &mut auths { *auth = self - .maybe_sign_soroban_authorization_entry(auth, network, current_ledger) + .maybe_sign_soroban_authorization_entry(auth, network, expiration_ledger) .await?; } body.auth = auths.try_into()?; @@ -136,7 +135,7 @@ pub trait Stellar { &self, unsigned_entry: &SorobanAuthorizationEntry, network: &Network, - current_ledger: u32, + expiration_ledger: u32, ) -> Result { if let SorobanAuthorizationEntry { credentials: SorobanCredentials::Address(SorobanAddressCredentials { address, .. }), @@ -160,7 +159,7 @@ pub trait Stellar { }; if key == self.get_public_key().await? { return self - .sign_soroban_authorization_entry(unsigned_entry, network, current_ledger) + .sign_soroban_authorization_entry(unsigned_entry, network, expiration_ledger) .await; } } @@ -176,7 +175,7 @@ pub trait Stellar { Network { network_passphrase, .. }: &Network, - current_ledger: u32, + expiration_ledger: u32, ) -> Result { let address = self.get_public_key().await?; let mut auth = unsigned_entry.clone(); @@ -194,7 +193,7 @@ pub trait Stellar { .. } = credentials; - *signature_expiration_ledger = current_ledger + 60; + *signature_expiration_ledger = expiration_ledger; let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { network_id: hash(network_passphrase),