Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,12 @@ Deploy a wasm contract
- `--instructions <INSTRUCTIONS>` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction
- `--instruction-leeway <INSTRUCTION_LEEWAY>` — Allow this many extra instructions when budgeting resources with transaction simulation
- `--cost` — Output the cost execution to stderr
- `--auth-mode <AUTH_MODE>` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions

Possible values:
- `enforce`: Validate the authorization entries already on the transaction
- `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation
- `non-root`: Record all authorization entries, including non-root entries

###### **Signing Options:**

Expand Down Expand Up @@ -864,6 +870,12 @@ Install a WASM file to the ledger without creating a contract instance
- `--instructions <INSTRUCTIONS>` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction
- `--instruction-leeway <INSTRUCTION_LEEWAY>` — Allow this many extra instructions when budgeting resources with transaction simulation
- `--cost` — Output the cost execution to stderr
- `--auth-mode <AUTH_MODE>` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions

Possible values:
- `enforce`: Validate the authorization entries already on the transaction
- `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation
- `non-root`: Record all authorization entries, including non-root entries

###### **Signing Options:**

Expand Down Expand Up @@ -921,6 +933,12 @@ Install a WASM file to the ledger without creating a contract instance
- `--instructions <INSTRUCTIONS>` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction
- `--instruction-leeway <INSTRUCTION_LEEWAY>` — Allow this many extra instructions when budgeting resources with transaction simulation
- `--cost` — Output the cost execution to stderr
- `--auth-mode <AUTH_MODE>` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions

Possible values:
- `enforce`: Validate the authorization entries already on the transaction
- `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation
- `non-root`: Record all authorization entries, including non-root entries

###### **Signing Options:**

Expand Down Expand Up @@ -978,6 +996,12 @@ stellar contract invoke ... -- --help
- `--instructions <INSTRUCTIONS>` — ⚠️ Deprecated, use `--instruction-leeway` to increase instructions. Number of instructions to allocate for the transaction
- `--instruction-leeway <INSTRUCTION_LEEWAY>` — Allow this many extra instructions when budgeting resources with transaction simulation
- `--cost` — Output the cost execution to stderr
- `--auth-mode <AUTH_MODE>` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions

Possible values:
- `enforce`: Validate the authorization entries already on the transaction
- `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation
- `non-root`: Record all authorization entries, including non-root entries

###### **Signing Options:**

Expand Down Expand Up @@ -3916,6 +3940,12 @@ Simulate a transaction envelope from stdin
- `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider, example: "X-API-Key: abc123". Multiple headers can be added by passing the option multiple times
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config
- `--auth-mode <AUTH_MODE>` — Set the authorization mode for transaction simulation. When unset, the RPC default is used: record with the root mode if no authorization entries exist, otherwise enforce the provided entries. Should only be set for `InvokeHostFunction` transactions

Possible values:
- `enforce`: Validate the authorization entries already on the transaction
- `root`: Record authorization entries, requiring each to be rooted at the transaction's top-level operation
- `non-root`: Record all authorization entries, including non-root entries

###### **Signing Options:**

Expand Down
73 changes: 71 additions & 2 deletions cmd/crates/soroban-test/tests/it/integration/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ async fn non_root_auth_with_authorized_subcall() {
.failure()
.stderr(predicates::str::contains("Auth, InvalidAction"));

// with source signer - expect failure
// TODO: this should pass once CLI supports non-root auth
// with source signer - expect failure due to default root auth mode
sandbox
.new_assert_cmd("contract")
.arg("invoke")
Expand All @@ -117,6 +116,76 @@ async fn non_root_auth_with_authorized_subcall() {
.stderr(predicates::str::contains("Auth, InvalidAction"));
}

#[tokio::test]
async fn non_root_auth_mode_signs_non_root_subcall() {
let sandbox = &TestEnv::new();
new_account(sandbox, "signer");

let (id1, id2) = deploy_auth_contracts(sandbox).await;

// with non-root auth mode, non-source signer, and auto-sign - expect success
sandbox
.new_assert_cmd("contract")
.arg("invoke")
.arg("--source=test")
.arg("--id")
.arg(&id1)
.arg("--auth-mode=non-root")
.arg("--auto-sign")
.arg("--")
.arg("no-auth-sub-auth")
.arg("--addr=signer")
.arg("--val=hello")
.arg(&format!("--subcall={id2}"))
.assert()
.success()
.stdout("\"hello\"\n");

// with non-root auth mode, source signer, and no auto-sign - expect success
// -> signature is covered by the envelope signature, no explicit signature needed
sandbox
.new_assert_cmd("contract")
.arg("invoke")
.arg("--source=test")
.arg("--id")
.arg(&id1)
.arg("--auth-mode=non-root")
.arg("--")
.arg("no-auth-sub-auth")
.arg("--addr=test")
.arg("--val=hello")
.arg(&format!("--subcall={id2}"))
.assert()
.success()
.stdout("\"hello\"\n");
}

#[tokio::test]
async fn non_root_auth_mode_via_env_var() {
let sandbox = &TestEnv::new();
new_account(sandbox, "signer");

let (id1, id2) = deploy_auth_contracts(sandbox).await;

// `STELLAR_AUTH_MODE` is the env-var equivalent of `--auth-mode`.
sandbox
.new_assert_cmd("contract")
.env("STELLAR_AUTH_MODE", "non-root")
.arg("invoke")
.arg("--source=test")
.arg("--id")
.arg(&id1)
.arg("--auto-sign")
.arg("--")
.arg("no-auth-sub-auth")
.arg("--addr=signer")
.arg("--val=hello")
.arg(&format!("--subcall={id2}"))
.assert()
.success()
.stdout("\"hello\"\n");
}

#[tokio::test]
async fn partial_auth_with_authorized_subcall() {
let sandbox = &TestEnv::new();
Expand Down
44 changes: 43 additions & 1 deletion cmd/crates/soroban-test/tests/it/integration/tx/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async fn simulate() {
.assert()
.success()
.stdout_as_str();
let assembled = simulate_and_assemble_transaction(&sandbox.client(), &tx, None, None)
let assembled = simulate_and_assemble_transaction(&sandbox.client(), &tx, None, None, None)
.await
.unwrap();
let txn_env: TransactionEnvelope = assembled.transaction().clone().into();
Expand All @@ -40,6 +40,48 @@ async fn simulate() {
);
}

#[tokio::test]
async fn simulate_auth_modes() {
let sandbox = &TestEnv::new();
let xdr_base64_build_only = deploy_contract(
sandbox,
HELLO_WORLD,
DeployOptions {
kind: DeployKind::BuildOnly,
salt: Some(String::from("B")),
..Default::default()
},
)
.await;

// The unset default and the recording modes assemble the deployer
// authorization the CreateContract op requires.
for args in [
&[][..],
&["--auth-mode=root"][..],
&["--auth-mode=non-root"][..],
] {
sandbox
.new_assert_cmd("tx")
.arg("simulate")
.args(args)
.write_stdin(xdr_base64_build_only.as_bytes())
.assert()
.success();
}

// `enforce` only validates authorization already present on the envelope.
// The build-only envelope has none, so it cannot authorize the deploy.
sandbox
.new_assert_cmd("tx")
.arg("simulate")
.arg("--auth-mode=enforce")
.write_stdin(xdr_base64_build_only.as_bytes())
.assert()
.failure()
.stderr(predicates::str::contains("Auth, InvalidAction"));
}

fn test_tx_string(sandbox: &TestEnv) -> String {
sandbox
.new_assert_cmd("contract")
Expand Down
7 changes: 5 additions & 2 deletions cmd/soroban-cli/src/assembled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ use stellar_xdr::curr::{
TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, VecM, WriteXdr,
};

use soroban_rpc::{Error, LogEvents, LogResources, ResourceConfig, SimulateTransactionResponse};
use soroban_rpc::{
AuthMode, Error, LogEvents, LogResources, ResourceConfig, SimulateTransactionResponse,
};

pub async fn simulate_and_assemble_transaction(
client: &soroban_rpc::Client,
tx: &Transaction,
resource_config: Option<ResourceConfig>,
resource_fee: Option<i64>,
auth_mode: Option<AuthMode>,
) -> Result<Assembled, Error> {
let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
tx: tx.clone(),
Expand All @@ -25,7 +28,7 @@ pub async fn simulate_and_assemble_transaction(
);

let sim_res = client
.next_simulate_transaction_envelope(&envelope, None, resource_config)
.next_simulate_transaction_envelope(&envelope, auth_mode, resource_config)
.await?;
tracing::trace!("{sim_res:#?}");

Expand Down
94 changes: 94 additions & 0 deletions cmd/soroban-cli/src/auth_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//! Auth mode for Soroban transaction simulation.
//!
//! Selects how the RPC handles authorization entries while simulating a
//! transaction. The variants map onto the RPC `simulateTransaction` `authMode`
//! parameter; leaving the argument unset omits the parameter and uses the RPC
//! default.

use clap::ValueEnum;

#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum AuthMode {
/// Validate the authorization entries already on the transaction.
Enforce,
/// Record authorization entries, requiring each to be rooted at the
/// transaction's top-level operation.
Root,
/// Record all authorization entries, including non-root entries.
#[value(name = "non-root")]
NonRoot,
}

impl AuthMode {
/// Map to the RPC `simulateTransaction` `authMode` parameter.
pub fn to_rpc(self) -> soroban_rpc::AuthMode {
match self {
AuthMode::Enforce => soroban_rpc::AuthMode::Enforce,
AuthMode::Root => soroban_rpc::AuthMode::Record,
AuthMode::NonRoot => soroban_rpc::AuthMode::RecordAllowNonRoot,
}
}
}

/// Shared `--auth-mode` argument for commands that simulate Soroban
/// transactions.
///
/// The argument is optional: when unset, no `authMode` is sent and the RPC uses
/// its default (record with root mode if no authorization entries exist,
/// otherwise enforce the provided entries). This is also the only safe behavior
/// for envelopes whose operation is not `InvokeHostFunction`, since the RPC
/// rejects `authMode` for those.
#[derive(Debug, clap::Args, Clone, Default)]
#[group(skip)]
pub struct Args {
/// Set the authorization mode for transaction simulation. When unset, the RPC
/// default is used: record with the root mode if no authorization entries
/// exist, otherwise enforce the provided entries. Should only be set for
/// `InvokeHostFunction` transactions.
#[arg(
long,
env = "STELLAR_AUTH_MODE",
help_heading = crate::commands::HEADING_RPC,
)]
pub auth_mode: Option<AuthMode>,
}

impl Args {
pub fn to_rpc(&self) -> Option<soroban_rpc::AuthMode> {
self.auth_mode.map(AuthMode::to_rpc)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn unset_omits_rpc_auth_mode() {
assert!(Args::default().to_rpc().is_none());
}

#[test]
fn enforce_maps_to_enforce() {
assert!(matches!(
AuthMode::Enforce.to_rpc(),
soroban_rpc::AuthMode::Enforce
));
}

#[test]
fn root_maps_to_record() {
assert!(matches!(
AuthMode::Root.to_rpc(),
soroban_rpc::AuthMode::Record
));
}

#[test]
fn non_root_maps_to_record_allow_non_root() {
assert!(matches!(
AuthMode::NonRoot.to_rpc(),
soroban_rpc::AuthMode::RecordAllowNonRoot
));
}
}
13 changes: 11 additions & 2 deletions cmd/soroban-cli/src/commands/contract/deploy/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,17 @@ impl Cmd {
return Ok(TxnResult::Txn(Box::new(tx)));
}

sim_sign_and_send_tx::<Error>(&client, &tx, config, &self.resources, &[], quiet, no_cache)
.await?;
sim_sign_and_send_tx::<Error>(
&client,
&tx,
config,
&self.resources,
&[],
None,
quiet,
no_cache,
)
.await?;

if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
print.linkln(url);
Expand Down
16 changes: 14 additions & 2 deletions cmd/soroban-cli/src/commands/contract/deploy/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub struct Cmd {
pub alias: Option<AliasName>,
#[command(flatten)]
pub resources: resources::Args,
#[command(flatten)]
pub auth_mode: crate::auth_mode::Args,
/// Build the transaction and only write the base64 xdr to stdout
#[arg(long, help_heading = HEADING_TRANSACTION)]
pub build_only: bool,
Expand Down Expand Up @@ -314,6 +316,7 @@ impl Cmd {
wasm: Some(wasm.clone()),
config: config.clone(),
resources: self.resources.clone(),
auth_mode: self.auth_mode.clone(),
ignore_checks: self.ignore_checks,
build_only: is_build,
package: None,
Expand Down Expand Up @@ -415,8 +418,17 @@ impl Cmd {
return Ok(TxnResult::Txn(txn));
}

sim_sign_and_send_tx::<Error>(&client, &txn, config, &self.resources, &[], quiet, no_cache)
.await?;
sim_sign_and_send_tx::<Error>(
&client,
&txn,
config,
&self.resources,
&[],
self.auth_mode.to_rpc(),
quiet,
no_cache,
)
.await?;

if let Some(url) = utils::lab_url_for_contract(&network, &contract_id) {
print.linkln(url);
Expand Down
3 changes: 3 additions & 0 deletions cmd/soroban-cli/src/commands/contract/extend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ impl Cmd {
config,
&self.resources,
&[],
// Footprint extend is not an InvokeHostFunction op, so the RPC does
// not accept an auth mode.
None,
quiet,
no_cache,
)
Expand Down
Loading
Loading