From f0d7b1d5666396314f1dc2568ede132ebb20361e Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Thu, 6 Jul 2023 13:56:30 -0400 Subject: [PATCH] Refactor authorization manager to only maintain mutable borrow on minimal amount of fields. This avoids auth manager re-borrow hack and allows calling `require_auth` from within `__check_auth`. The latter might not be the most important feature, but it makes things more consistent. --- soroban-env-host/src/auth.rs | 654 +++++++++++++----- soroban-env-host/src/host.rs | 14 +- soroban-env-host/src/host/frame.rs | 57 +- soroban-env-host/src/test/auth.rs | 285 +++++++- soroban-env-host/src/test/storage.rs | 8 +- soroban-env-host/src/test/token.rs | 24 +- soroban-test-wasms/src/lib.rs | 2 + soroban-test-wasms/wasm-workspace/Cargo.lock | 63 +- soroban-test-wasms/wasm-workspace/Cargo.toml | 5 +- .../wasm-workspace/auth/src/lib.rs | 11 + .../wasm-workspace/complex/src/lib.rs | 4 +- .../wasm-workspace/contract_data/src/lib.rs | 12 +- .../delegated_account/Cargo.toml | 14 + .../delegated_account/src/lib.rs | 37 + .../opt/auth_test_contract.wasm | Bin 5421 -> 5764 bytes .../wasm-workspace/opt/example_add_f32.wasm | Bin 1102 -> 1102 bytes .../wasm-workspace/opt/example_add_i32.wasm | Bin 1049 -> 1049 bytes .../wasm-workspace/opt/example_complex.wasm | Bin 1843 -> 1843 bytes .../opt/example_contract_data.wasm | Bin 3229 -> 3099 bytes .../opt/example_create_contract.wasm | Bin 712 -> 712 bytes .../wasm-workspace/opt/example_fannkuch.wasm | Bin 1044 -> 1044 bytes .../wasm-workspace/opt/example_fib.wasm | Bin 360 -> 360 bytes .../wasm-workspace/opt/example_hostile.wasm | Bin 1166 -> 1166 bytes .../opt/example_invoke_contract.wasm | Bin 2000 -> 2000 bytes .../opt/example_linear_memory.wasm | Bin 854 -> 854 bytes .../opt/example_simple_account.wasm | Bin 2070 -> 2070 bytes .../opt/example_updateable_contract.wasm | Bin 606 -> 606 bytes .../wasm-workspace/opt/example_vec.wasm | Bin 594 -> 594 bytes .../opt/test_delegated_account.wasm | Bin 0 -> 2052 bytes .../wasm-workspace/simple_account/src/lib.rs | 8 +- 30 files changed, 896 insertions(+), 302 deletions(-) create mode 100644 soroban-test-wasms/wasm-workspace/delegated_account/Cargo.toml create mode 100644 soroban-test-wasms/wasm-workspace/delegated_account/src/lib.rs create mode 100644 soroban-test-wasms/wasm-workspace/opt/test_delegated_account.wasm diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs index 7de7b84d4..6d98e0d24 100644 --- a/soroban-env-host/src/auth.rs +++ b/soroban-env-host/src/auth.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -11,6 +12,7 @@ use soroban_env_common::xdr::{ }; use soroban_env_common::{AddressObject, Compare, Symbol, TryFromVal, TryIntoVal, Val, VecObject}; +use crate::host::error::TryBorrowOrErr; use crate::host::metered_clone::{charge_container_bulk_init_with_elts, MeteredClone}; use crate::host::Frame; use crate::host_object::HostVec; @@ -37,17 +39,71 @@ pub struct AuthorizationManager { // Per-address trackers of authorized invocations. // Every tracker takes care about a single rooted invocation tree for some // address. There can be multiple trackers per address. - account_trackers: Vec, + // The internal structure of this field is build in such a way that trackers + // can be borrowed mutably independently, while still allowing for + // modification of the `account_trackers` vec itself. + account_trackers: RefCell>>, // Per-address trackers for authorization performed by the contracts at // execution time (as opposed to signature-based authorization for accounts). // Contract authorizations are always enforced independently of the `mode`, // as they are self-contained and fully defined by the contract logic. - invoker_contract_trackers: Vec, + invoker_contract_trackers: RefCell>, // Current call stack consisting only of the contract invocations (i.e. not // the host functions). - call_stack: Vec, + call_stack: RefCell>, } +macro_rules! impl_checked_borrow_helpers { + ($field:ident, $t:ty, $borrow:ident, $borrow_mut:ident) => { + impl AuthorizationManager { + #[allow(dead_code)] + fn $borrow(&self, host: &Host) -> Result, HostError> { + use crate::host::error::TryBorrowOrErr; + self.$field.try_borrow_or_err_with( + host, + concat!( + "authorization_manager.", + stringify!($field), + ".try_borrow failed" + ), + ) + } + fn $borrow_mut(&self, host: &Host) -> Result, HostError> { + use crate::host::error::TryBorrowOrErr; + self.$field.try_borrow_mut_or_err_with( + host, + concat!( + "authorization_manager.", + stringify!($field), + ".try_borrow_mut failed" + ), + ) + } + } + }; +} + +impl_checked_borrow_helpers!( + account_trackers, + Vec>, + try_borrow_account_trackers, + try_borrow_account_trackers_mut +); + +impl_checked_borrow_helpers!( + invoker_contract_trackers, + Vec, + try_borrow_invoker_contract_trackers, + try_borrow_invoker_contract_trackers_mut +); + +impl_checked_borrow_helpers!( + call_stack, + Vec, + try_borrow_call_stack, + try_borrow_call_stack_mut +); + // The authorization payload recorded for an address in the recording // authorization mode. #[derive(Debug)] @@ -59,17 +115,26 @@ pub struct RecordedAuthPayload { // Snapshot of `AuthorizationManager` to use when performing the callstack // rollbacks. -#[derive(Clone)] pub struct AuthorizationManagerSnapshot { - // AccountAuthorizationTracker has some immutable parts, but it is safer to - // just rollback everything. If this is an issue, then the - // AccountAuthorizationTracker should probably be separated into 'mutable' - // and 'immutable' parts. - account_trackers: Vec, - invoker_contract_trackers: Vec, + account_trackers_snapshot: AccountTrackersSnapshot, + invoker_contract_tracker_root_snapshots: Vec, + // This is set only for the recording mode. tracker_by_address_handle: Option>, } +// Snapshot of the `account_trackers` in `AuthorizationManager`. +enum AccountTrackersSnapshot { + // In enforcing mode we only need to snapshot the mutable part of the + // trackers. + // `None` means that the tracker is currently in authentication process and + // shouldn't be modified (as the tracker can't be used to authenticate + // itself). + Enforcing(Vec>), + // In recording mode snapshot the whole vector, as we create trackers + // lazily and hence the outer vector itself might change. + Recording(Vec>), +} + #[derive(Clone)] enum AuthorizationMode { Enforcing, @@ -84,7 +149,28 @@ struct RecordingAuthInfo { // This allows to disambiguate between the addresses that have the same // value, but are specified as two different objects (e.g. as two different // contract function inputs). - tracker_by_address_handle: HashMap, + tracker_by_address_handle: RefCell>, +} + +impl RecordingAuthInfo { + fn try_borrow_tracker_by_address_handle( + &self, + host: &Host, + ) -> Result>, HostError> { + self.tracker_by_address_handle.try_borrow_or_err_with( + host, + "recording_auth_info.tracker_by_address_handle.try_borrow failed", + ) + } + fn try_borrow_tracker_by_address_handle_mut( + &self, + host: &Host, + ) -> Result>, HostError> { + self.tracker_by_address_handle.try_borrow_mut_or_err_with( + host, + "recording_auth_info.tracker_by_address_handle.try_borrow_mut failed", + ) + } } // Helper for matching the 'sparse' tree of authorized invocations to the actual @@ -115,6 +201,8 @@ struct AccountAuthorizationTracker { // Tracked address. If `None`, then lazily set to the transaction source // account's (i.e. invoker's) address is used. address: Option, + // Helper for matching the tree that address authorized to the invocation + // tree. invocation_tracker: InvocationTracker, // Arguments representing the signature(s) made by the address to authorize // the invocations tracked here. @@ -127,12 +215,18 @@ struct AccountAuthorizationTracker { // Indicates whether authentication has already succesfully happened. authenticated: bool, // Indicates whether nonce still needs to be verified and consumed. - need_nonce: bool, + is_nonce_consumed: bool, // The value of nonce authorized by the address with its expiration ledger. // Must not exist in the ledger. nonce: Option<(i64, u32)>, } +struct AccountAuthorizationTrackerSnapshot { + invocation_tracker_root_snapshot: AuthorizedInvocationSnapshot, + authenticated: bool, + is_nonce_consumed: bool, +} + // Stores all the authorizations peformed by contracts at runtime. #[derive(Clone)] struct InvokerContractAuthorizationTracker { @@ -178,6 +272,12 @@ pub(crate) struct AuthorizedInvocation { is_exhausted: bool, } +// Snapshot of `AuthorizedInvocation` that contains only mutable fields. +struct AuthorizedInvocationSnapshot { + is_exhausted: bool, + sub_invocations: Vec, +} + impl Compare for Host { type Error = HostError; @@ -233,9 +333,7 @@ impl AuthStackFrame { match self { AuthStackFrame::Contract(contract_frame) => { Ok(AuthorizedFunction::ContractFn(ContractFunction { - contract_address: contract_frame - .contract_address - .metered_clone(host.budget_ref())?, + contract_address: contract_frame.contract_address, function_name: contract_frame .function_name .metered_clone(host.budget_ref())?, @@ -410,6 +508,25 @@ impl AuthorizedInvocation { } self } + + fn snapshot(&self) -> AuthorizedInvocationSnapshot { + AuthorizedInvocationSnapshot { + is_exhausted: self.is_exhausted, + sub_invocations: self.sub_invocations.iter().map(|i| i.snapshot()).collect(), + } + } + + fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> { + self.is_exhausted = snapshot.is_exhausted; + if self.sub_invocations.len() != snapshot.sub_invocations.len() { + // This would be a bug. + return Err((ScErrorType::Auth, ScErrorCode::InternalError).into()); + } + for i in 0..self.sub_invocations.len() { + self.sub_invocations[i].rollback(&snapshot.sub_invocations[i])?; + } + Ok(()) + } } impl Default for AuthorizationManager { @@ -428,15 +545,15 @@ impl AuthorizationManager { ) -> Result { let mut trackers = vec![]; for auth_entry in auth_entries { - trackers.push(AccountAuthorizationTracker::from_authorization_entry( - host, auth_entry, - )?); + trackers.push(RefCell::new( + AccountAuthorizationTracker::from_authorization_entry(host, auth_entry)?, + )); } Ok(Self { mode: AuthorizationMode::Enforcing, - call_stack: vec![], - account_trackers: trackers, - invoker_contract_trackers: vec![], + call_stack: RefCell::new(vec![]), + account_trackers: RefCell::new(trackers), + invoker_contract_trackers: RefCell::new(vec![]), }) } @@ -446,9 +563,9 @@ impl AuthorizationManager { pub(crate) fn new_enforcing_without_authorizations() -> Self { Self { mode: AuthorizationMode::Enforcing, - call_stack: vec![], - account_trackers: vec![], - invoker_contract_trackers: vec![], + call_stack: RefCell::new(vec![]), + account_trackers: RefCell::new(vec![]), + invoker_contract_trackers: RefCell::new(vec![]), } } @@ -460,9 +577,9 @@ impl AuthorizationManager { mode: AuthorizationMode::Recording(RecordingAuthInfo { tracker_by_address_handle: Default::default(), }), - call_stack: vec![], - account_trackers: vec![], - invoker_contract_trackers: vec![], + call_stack: RefCell::new(vec![]), + account_trackers: RefCell::new(vec![]), + invoker_contract_trackers: RefCell::new(vec![]), } } @@ -472,68 +589,73 @@ impl AuthorizationManager { // In the recording mode this stores the auth requirement instead of // verifying it. pub(crate) fn require_auth( - &mut self, + &self, host: &Host, address: AddressObject, args: Vec, ) -> Result<(), HostError> { - let curr_invocation = self.call_stack.last().ok_or_else(|| { - host.err( - ScErrorType::Auth, - ScErrorCode::InternalError, - "unexpected require_auth outside of valid frame", - &[], - ) - })?; - self.require_auth_internal( - host, - address, - curr_invocation.to_authorized_function(host, args)?, - ) + let authorized_function = self + .try_borrow_call_stack(host)? + .last() + .ok_or_else(|| { + host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected require_auth outside of valid frame", + &[], + ) + })? + .to_authorized_function(host, args)?; + + self.require_auth_internal(host, address, authorized_function) } pub(crate) fn add_invoker_contract_auth( - &mut self, + &self, host: &Host, auth_entries: VecObject, ) -> Result<(), HostError> { let auth_entries = host.visit_obj(auth_entries, |e: &HostVec| e.to_vec(host.budget_ref()))?; + let mut trackers = self.try_borrow_invoker_contract_trackers_mut(host)?; for e in auth_entries { - self.invoker_contract_trackers - .push(InvokerContractAuthorizationTracker::new(host, e)?) + trackers.push(InvokerContractAuthorizationTracker::new(host, e)?) } Ok(()) } fn verify_contract_invoker_auth( - &mut self, + &self, host: &Host, address: AddressObject, function: &AuthorizedFunction, ) -> Result { - // If stack has just one call there can't be invoker. - if self.call_stack.len() < 2 { - return Ok(false); - } + { + let call_stack = self.try_borrow_call_stack(host)?; + // If stack has just one call there can't be invoker. + if call_stack.len() < 2 { + return Ok(false); + } - // Try matching the direct invoker contract first. It is considered to - // have authorized any direct calls. - let invoker_frame = &self.call_stack[self.call_stack.len() - 2]; - if let AuthStackFrame::Contract(invoker_contract) = invoker_frame { - if host - .compare(&invoker_contract.contract_address, &address)? - .is_eq() - { - return Ok(true); + // Try matching the direct invoker contract first. It is considered to + // have authorized any direct calls. + let invoker_frame = &call_stack[call_stack.len() - 2]; + if let AuthStackFrame::Contract(invoker_contract) = invoker_frame { + if host + .compare(&invoker_contract.contract_address, &address)? + .is_eq() + { + return Ok(true); + } } } + let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?; // If there is no direct invoker, there still might be a valid // sub-contract call authorization from another invoker higher up the // stack. Note, that invoker contract trackers consider the direct frame // to never require auth (any `require_auth` calls would be matched by // logic above). - for tracker in &mut self.invoker_contract_trackers { + for tracker in invoker_contract_trackers.iter_mut() { if host.compare(&tracker.contract_address, &address)?.is_eq() && tracker.maybe_authorize_invocation(host, function)? { @@ -545,48 +667,60 @@ impl AuthorizationManager { } fn require_auth_enforcing( - &mut self, + &self, host: &Host, address: AddressObject, function: &AuthorizedFunction, ) -> Result<(), HostError> { // Iterate all the trackers and try to find one that // fullfills the authorization requirement. - for tracker in &mut self.account_trackers { - let address_matches = if let Some(addr) = &tracker.address { - host.compare(addr, &address)?.is_eq() - } else { - // Lazily fill the address for the invoker trackers, - // so that it's possible to create the auth manager - // without knowing the invoker beforehand and also - // to not keep calling into `source_account` function. - let source_addr = host.source_account_address()?.ok_or_else(|| { - host.err( - ScErrorType::Auth, - ScErrorCode::InternalError, - "unexpected missing invoker in auth manager", - &[], - ) - })?; - let source_matches = host.compare(&source_addr, &address)?.is_eq(); - tracker.address = Some(source_addr); - source_matches - }; - // If address doesn't match, just skip the tracker. - if !address_matches { - continue; - } - match tracker.maybe_authorize_invocation(host, function) { - // If tracker doesn't have a matching invocation, - // just skip it (there could still be another - // tracker that matches it). - Ok(false) => continue, - // Found a matching authorization. - Ok(true) => return Ok(()), - // Found a matching authorization, but another - // requirement hasn't been fullfilled (for - // example, incorrect authentication or nonce). - Err(e) => return Err(e), + for tracker in self.try_borrow_account_trackers(host)?.iter() { + // Tracker can only be borrowed by the authorization manager itself. + // The only scenario in which re-borrow might occur is when + // `require_auth` is called within `__check_auth` call. The tracker + // that called `__check_auth` would be already borrowed in such + // scenario. + // We allow such call patterns in general, but we don't allow using + // tracker to verify auth for itself, i.e. we don't allow something + // like address.require_auth()->address_contract.__check_auth() + // ->address.require_auth(). Thus we simply skip the trackers that + // have already been borrowed. + if let Ok(mut tracker) = tracker.try_borrow_mut() { + let address_matches = if let Some(addr) = &tracker.address { + host.compare(addr, &address)?.is_eq() + } else { + // Lazily fill the address for the invoker trackers, + // so that it's possible to create the auth manager + // without knowing the invoker beforehand and also + // to not keep calling into `source_account` function. + let source_addr = host.source_account_address()?.ok_or_else(|| { + host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected missing invoker in auth manager", + &[], + ) + })?; + let source_matches = host.compare(&source_addr, &address)?.is_eq(); + tracker.address = Some(source_addr); + source_matches + }; + // If address doesn't match, just skip the tracker. + if !address_matches { + continue; + } + match tracker.maybe_authorize_invocation(host, function) { + // If tracker doesn't have a matching invocation, + // just skip it (there could still be another + // tracker that matches it). + Ok(false) => continue, + // Found a matching authorization. + Ok(true) => return Ok(()), + // Found a matching authorization, but another + // requirement hasn't been fullfilled (for + // example, incorrect authentication or nonce). + Err(e) => return Err(e), + } } } // No matching tracker found, hence the invocation isn't @@ -600,7 +734,7 @@ impl AuthorizationManager { } fn require_auth_internal( - &mut self, + &self, host: &Host, address: AddressObject, function: AuthorizedFunction, @@ -613,86 +747,196 @@ impl AuthorizationManager { return Ok(()); } - match &mut self.mode { + match &self.mode { AuthorizationMode::Enforcing => self.require_auth_enforcing(host, address, &function), AuthorizationMode::Recording(recording_info) => { let address_obj_handle = address.get_handle(); - if let Some(tracker_id) = recording_info - .tracker_by_address_handle + let existing_tracker_id = recording_info + .try_borrow_tracker_by_address_handle(host)? .get(&address_obj_handle) - { - let tracker = &mut self.account_trackers[*tracker_id]; - // The recording invariant is that trackers are created - // with the first authorized invocation, which means - // that when their stack no longer has authorized - // invocation, then we've popped frames past its root - // and hence need to create a new tracker. - if !tracker.has_authorized_invocations_in_stack() { - recording_info - .tracker_by_address_handle - .remove(&address_obj_handle); + .copied(); + if let Some(tracker_id) = existing_tracker_id { + // The tracker should not be borrowed recursively in + // recording mode, as we don't call `__check_auth` in this + // flow. + if let Ok(mut tracker) = + self.try_borrow_account_trackers(host)?[tracker_id].try_borrow_mut() + { + // The recording invariant is that trackers are created + // with the first authorized invocation, which means + // that when their stack no longer has authorized + // invocation, then we've popped frames past its root + // and hence need to create a new tracker. + if !tracker.has_authorized_invocations_in_stack() { + recording_info + .try_borrow_tracker_by_address_handle_mut(host)? + .remove(&address_obj_handle); + } else { + return tracker.record_invocation(host, function); + } } else { - return self.account_trackers[*tracker_id] - .record_invocation(host, function); + return Err(host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected recursive tracker borrow in recording mode", + &[], + )); } } // If a tracker for the new tree doesn't exist yet, create // it and initialize with the current invocation. - self.account_trackers - .push(AccountAuthorizationTracker::new_recording( + self.try_borrow_account_trackers_mut(host)? + .push(RefCell::new(AccountAuthorizationTracker::new_recording( host, address, function, - self.call_stack.len(), - )?); + self.try_borrow_call_stack(host)?.len(), + )?)); recording_info - .tracker_by_address_handle - .insert(address_obj_handle, self.account_trackers.len() - 1); + .try_borrow_tracker_by_address_handle_mut(host)? + .insert( + address_obj_handle, + self.try_borrow_account_trackers(host)?.len() - 1, + ); Ok(()) } } } // Returns a snapshot of `AuthorizationManager` to use for rollback. - pub(crate) fn snapshot(&self) -> AuthorizationManagerSnapshot { + pub(crate) fn snapshot(&self, host: &Host) -> Result { + let account_trackers_snapshot = match &self.mode { + AuthorizationMode::Enforcing => AccountTrackersSnapshot::Enforcing( + self.try_borrow_account_trackers(host)? + .iter() + .map(|t| { + if let Ok(tracker) = t.try_borrow() { + Some(tracker.snapshot()) + } else { + // If tracker is borrowed, snapshotting it is a no-op + // (it can't change until we release it higher up the + // stack). + None + } + }) + .collect(), + ), + AuthorizationMode::Recording(_) => { + // All trackers should be avaialable to borrow for copy as in + // recording mode we can't have recursive authorization. + AccountTrackersSnapshot::Recording(self.try_borrow_account_trackers(host)?.clone()) + } + }; + let invoker_contract_tracker_root_snapshots = self + .try_borrow_invoker_contract_trackers(host)? + .iter() + .map(|t| t.invocation_tracker.root_authorized_invocation.snapshot()) + .collect(); let tracker_by_address_handle = match &self.mode { AuthorizationMode::Enforcing => None, - AuthorizationMode::Recording(recording_info) => { - Some(recording_info.tracker_by_address_handle.clone()) - } + AuthorizationMode::Recording(recording_info) => Some( + recording_info + .try_borrow_tracker_by_address_handle(host)? + .clone(), + ), }; - AuthorizationManagerSnapshot { - account_trackers: self.account_trackers.clone(), - invoker_contract_trackers: self.invoker_contract_trackers.clone(), + Ok(AuthorizationManagerSnapshot { + account_trackers_snapshot, + invoker_contract_tracker_root_snapshots, tracker_by_address_handle, - } + }) } // Rolls back this `AuthorizationManager` to the snapshot state. - pub(crate) fn rollback(&mut self, snapshot: AuthorizationManagerSnapshot) { - self.account_trackers = snapshot.account_trackers; - self.invoker_contract_trackers = snapshot.invoker_contract_trackers; + pub(crate) fn rollback( + &self, + host: &Host, + snapshot: AuthorizationManagerSnapshot, + ) -> Result<(), HostError> { + match snapshot.account_trackers_snapshot { + AccountTrackersSnapshot::Enforcing(trackers_snapshot) => { + let trackers = self.try_borrow_account_trackers(host)?; + if trackers.len() != trackers_snapshot.len() { + return Err(host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected bad auth snapshot", + &[], + )); + } + for i in 0..trackers.len() { + if let Some(tracker_snapshot) = &trackers_snapshot[i] { + trackers[i] + .try_borrow_mut() + .map_err(|_| { + host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected bad auth snapshot", + &[], + ) + })? + .rollback(&tracker_snapshot)?; + } + } + } + AccountTrackersSnapshot::Recording(s) => { + *self.try_borrow_account_trackers_mut(host)? = s; + } + } + + let invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?; + if invoker_trackers.len() != snapshot.invoker_contract_tracker_root_snapshots.len() { + return Err(host.err( + ScErrorType::Auth, + ScErrorCode::InternalError, + "unexpected bad auth snapshot", + &[], + )); + } + if let Some(tracker_by_address_handle) = snapshot.tracker_by_address_handle { - match &mut self.mode { + match &self.mode { AuthorizationMode::Recording(recording_info) => { - recording_info.tracker_by_address_handle = tracker_by_address_handle; + *recording_info.try_borrow_tracker_by_address_handle_mut(host)? = + tracker_by_address_handle; } AuthorizationMode::Enforcing => (), } } + Ok(()) } - pub(crate) fn push_create_contract_host_fn_frame(&mut self, args: CreateContractArgs) { - self.call_stack - .push(AuthStackFrame::CreateContractHostFn(args)); - for tracker in &mut self.account_trackers { + fn push_tracker_frame(&self, host: &Host) -> Result<(), HostError> { + for tracker in self.try_borrow_account_trackers(host)?.iter() { + // Skip already borrowed trackers, these must be in the middle of + // authentication and hence don't need stack to be updated. + if let Ok(mut tracker) = tracker.try_borrow_mut() { + tracker.push_frame(); + } + } + for tracker in self + .try_borrow_invoker_contract_trackers_mut(host)? + .iter_mut() + { tracker.push_frame(); } + Ok(()) + } + + pub(crate) fn push_create_contract_host_fn_frame( + &self, + host: &Host, + args: CreateContractArgs, + ) -> Result<(), HostError> { + self.try_borrow_call_stack_mut(host)? + .push(AuthStackFrame::CreateContractHostFn(args)); + self.push_tracker_frame(host) } // Records a new call stack frame. // This should be called for every `Host` `push_frame`. - pub(crate) fn push_frame(&mut self, host: &Host, frame: &Frame) -> Result<(), HostError> { + pub(crate) fn push_frame(&self, host: &Host, frame: &Frame) -> Result<(), HostError> { let (contract_id, function_name) = match frame { Frame::ContractVM(vm, fn_name, ..) => { (vm.contract_id.metered_clone(host.budget_ref())?, *fn_name) @@ -709,45 +953,52 @@ impl AuthorizationManager { }; // Currently only contracts might appear in the call stack. let contract_address = host.add_host_object(ScAddress::Contract(contract_id))?; - self.call_stack + self.try_borrow_call_stack_mut(host)? .push(AuthStackFrame::Contract(ContractInvocation { contract_address, function_name, })); - for tracker in &mut self.account_trackers { - tracker.push_frame(); - } - for tracker in &mut self.invoker_contract_trackers { - tracker.push_frame(); - } - Ok(()) + + self.push_tracker_frame(host) } // Pops a call stack frame. // This should be called for every `Host` `pop_frame`. - pub(crate) fn pop_frame(&mut self) { - // Currently we don't push host function call frames, hence this may be - // called with empty stack. We trust the Host to keep things correct, - // i.e. that only host function frames are ignored this way. - // Eventually we may want to also authorize host fns via this, so this - // won't be needed. - if self.call_stack.is_empty() { - return; + pub(crate) fn pop_frame(&self, host: &Host) -> Result<(), HostError> { + { + let mut call_stack = self.try_borrow_call_stack_mut(host)?; + // Currently we don't push host function call frames, hence this may be + // called with empty stack. We trust the Host to keep things correct, + // i.e. that only host function frames are ignored this way. + if call_stack.is_empty() { + return Ok(()); + } + call_stack.pop(); } - self.call_stack.pop(); - for tracker in &mut self.account_trackers { - tracker.pop_frame(); + for tracker in self.try_borrow_account_trackers(host)?.iter() { + // Skip already borrowed trackers, these must be in the middle of + // authentication and hence don't need stack to be updated. + if let Ok(mut tracker) = tracker.try_borrow_mut() { + tracker.pop_frame(); + } } - let mut i = 0; - while i < self.invoker_contract_trackers.len() { - self.invoker_contract_trackers[i].pop_frame(); - if self.invoker_contract_trackers[i].is_out_of_scope() { - self.invoker_contract_trackers.swap_remove(i); + let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?; + for tracker in invoker_contract_trackers.iter_mut() { + tracker.pop_frame(); + } + // Pop invoker contract trackers that went out of scope. The invariant + // is that tracker only exists for the next sub-contract call (or until + // the tracker's frame itself is popped). Thus trackers form a stack + // where the shorter lifetime trackers are at the top. + while let Some(last) = invoker_contract_trackers.last() { + if last.is_out_of_scope() { + invoker_contract_trackers.pop(); } else { - i += 1; + break; } } + Ok(()) } // Returns the recorded per-address authorization payloads that would cover the @@ -763,9 +1014,9 @@ impl AuthorizationManager { ScErrorCode::InternalError, ))), AuthorizationMode::Recording(_) => Ok(self - .account_trackers + .try_borrow_account_trackers(host)? .iter() - .map(|tracker| tracker.get_recorded_auth_payload(host)) + .map(|tracker| tracker.try_borrow_or_err()?.get_recorded_auth_payload(host)) .collect::, HostError>>()?), } } @@ -779,8 +1030,10 @@ impl AuthorizationManager { match &self.mode { AuthorizationMode::Enforcing => Ok(()), AuthorizationMode::Recording(_) => { - for tracker in &self.account_trackers { - tracker.emulate_authentication(host)?; + for tracker in self.try_borrow_account_trackers(host)?.iter() { + tracker + .try_borrow_mut_or_err()? + .emulate_authentication(host)?; } Ok(()) } @@ -807,24 +1060,20 @@ impl AuthorizationManager { host: &Host, ) -> Vec<(ScAddress, xdr::SorobanAuthorizedInvocation)> { self.account_trackers + .borrow() .iter() - .filter(|t| t.authenticated && t.is_exhausted()) - .filter_map(|t| { - // Ignore authorizations without an address as they are implied, - // less useful as a test utility, and not succinctly capturable - // in the list of tuples. This is a tradeoff between offering up - // all authorizations vs the authorizations developers will - // mostly care about at the benefit of making this list easier - // to use. - t.address.as_ref().map(|a| { - ( - host.scaddress_from_address(*a).unwrap(), - t.invocation_tracker - .root_authorized_invocation - .to_xdr_non_metered(host, true) - .unwrap(), - ) - }) + .filter(|t| t.borrow().authenticated) + .map(|t| { + ( + // Authenticated trackers must have the address already set. + host.scaddress_from_address(t.borrow().address.unwrap()) + .unwrap(), + t.borrow() + .invocation_tracker + .root_authorized_invocation + .to_xdr_non_metered(host, true) + .unwrap(), + ) }) .collect() } @@ -995,7 +1244,7 @@ impl AccountAuthorizationTracker { invocation_tracker: InvocationTracker::from_xdr(host, auth_entry.root_invocation)?, signature_args, authenticated: false, - need_nonce: !is_invoker, + is_nonce_consumed: !is_invoker, is_invoker, nonce, is_valid: true, @@ -1027,7 +1276,7 @@ impl AccountAuthorizationTracker { }; let nonce = if !is_invoker { let random_nonce: i64 = rand::thread_rng().gen_range(0, i64::MAX); - host.consume_nonce(address.metered_clone(host.budget_ref())?, random_nonce, 0)?; + host.consume_nonce(address, random_nonce, 0)?; Some((random_nonce, 0)) } else { None @@ -1044,7 +1293,7 @@ impl AccountAuthorizationTracker { signature_args: Default::default(), is_valid: true, authenticated: true, - need_nonce: false, + is_nonce_consumed: false, is_invoker, nonce, }) @@ -1094,7 +1343,7 @@ impl AccountAuthorizationTracker { err } }) - .and_then(|_| self.verify_nonce(host)); + .and_then(|_| self.verify_and_consume_nonce(host)); if let Some(err) = authenticate_res.err() { self.is_valid = false; return Err(err); @@ -1168,11 +1417,11 @@ impl AccountAuthorizationTracker { self.invocation_tracker.pop_frame(); } - fn verify_nonce(&mut self, host: &Host) -> Result<(), HostError> { - if !self.need_nonce { + fn verify_and_consume_nonce(&mut self, host: &Host) -> Result<(), HostError> { + if !self.is_nonce_consumed { return Ok(()); } - self.need_nonce = false; + self.is_nonce_consumed = false; if let Some((nonce, expiration_ledger)) = &self.nonce { let (ledger_seq, max_entry_expiration) = host.with_ledger_info(|li| Ok((li.sequence_number, li.max_entry_expiration)))?; @@ -1301,11 +1550,32 @@ impl AccountAuthorizationTracker { } } - #[cfg(any(test, feature = "testutils"))] - fn is_exhausted(&self) -> bool { + fn snapshot(&self) -> AccountAuthorizationTrackerSnapshot { + AccountAuthorizationTrackerSnapshot { + invocation_tracker_root_snapshot: self + .invocation_tracker + .root_authorized_invocation + .snapshot(), + // We need to snapshot authenctioncation and nocne-related fields as + // during the rollback the nonce might be 'un-consumed' during + // the storage rollback. We don't snapshot `is_valid` since this is + // not recoverable and nonce is never consumed for invalid + // authorizations. + authenticated: self.authenticated, + is_nonce_consumed: self.is_nonce_consumed, + } + } + + fn rollback( + &mut self, + snapshot: &AccountAuthorizationTrackerSnapshot, + ) -> Result<(), HostError> { self.invocation_tracker .root_authorized_invocation - .is_exhausted + .rollback(&snapshot.invocation_tracker_root_snapshot)?; + self.authenticated = snapshot.authenticated; + self.is_nonce_consumed = snapshot.is_nonce_consumed; + Ok(()) } } diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index b36842c64..d58f8ea6a 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -573,8 +573,8 @@ impl Host { ) -> Result { let has_deployer = deployer.is_some(); if has_deployer { - self.try_borrow_authorization_manager_mut()? - .push_create_contract_host_fn_frame(args.metered_clone(self.budget_ref())?); + self.try_borrow_authorization_manager()? + .push_create_contract_host_fn_frame(self, args.metered_clone(self.budget_ref())?)?; } // Make sure that even in case of operation failure we still pop the // stack frame. @@ -584,7 +584,7 @@ impl Host { // for them just to make auth work in a single case). let res = self.create_contract_with_optional_auth(deployer, args); if has_deployer { - self.try_borrow_authorization_manager_mut()?.pop_frame(); + self.try_borrow_authorization_manager()?.pop_frame(self)?; } res } @@ -595,7 +595,7 @@ impl Host { args: CreateContractArgs, ) -> Result { if let Some(deployer_address) = deployer { - self.try_borrow_authorization_manager_mut()?.require_auth( + self.try_borrow_authorization_manager()?.require_auth( self, deployer_address, Default::default(), @@ -2881,7 +2881,7 @@ impl VmCallerEnv for Host { ) -> Result { let args = self.visit_obj(args, |a: &HostVec| a.to_vec(self.budget_ref()))?; Ok(self - .try_borrow_authorization_manager_mut()? + .try_borrow_authorization_manager()? .require_auth(self, address, args)? .into()) } @@ -2910,7 +2910,7 @@ impl VmCallerEnv for Host { })?; Ok(self - .try_borrow_authorization_manager_mut()? + .try_borrow_authorization_manager()? .require_auth(self, address, args)? .into()) } @@ -2921,7 +2921,7 @@ impl VmCallerEnv for Host { auth_entries: VecObject, ) -> Result { Ok(self - .try_borrow_authorization_manager_mut()? + .try_borrow_authorization_manager()? .add_invoker_contract_auth(self, auth_entries)? .into()) } diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs index bb23b6e31..81ad16c87 100644 --- a/soroban-env-host/src/host/frame.rs +++ b/soroban-env-host/src/host/frame.rs @@ -47,11 +47,11 @@ const RESERVED_CONTRACT_FN_PREFIX: &str = "__"; /// Saves host state (storage and objects) for rolling back a (sub-)transaction /// on error. A helper type used by [`FrameGuard`]. // Notes on metering: `RollbackPoint` are metered under Frame operations -#[derive(Clone)] +// #[derive(Clone)] pub(super) struct RollbackPoint { storage: StorageMap, events: usize, - auth: Option, + auth: AuthorizationManagerSnapshot, } #[cfg(any(test, feature = "testutils"))] @@ -117,15 +117,10 @@ impl Host { /// operation fails, it can be used to roll the [`Host`] back to the state /// it had before its associated [`Frame`] was pushed. pub(super) fn push_frame(&self, frame: Frame) -> Result { - // This is a bit hacky, as it relies on re-borrow to occur only during - // the account contract invocations. Instead we should probably call it - // in more explicitly different fashion and check if we're calling it - // instead of a borrow check. - let mut auth_snapshot = None; - if let Ok(mut auth_manager) = self.0.authorization_manager.try_borrow_mut() { - auth_manager.push_frame(self, &frame)?; - auth_snapshot = Some(auth_manager.snapshot()); - } + let auth_manager = self.try_borrow_authorization_manager()?; + let auth_snapshot = auth_manager.snapshot(self)?; + auth_manager.push_frame(self, &frame)?; + let ctx = Context { frame, prng: None, @@ -144,7 +139,7 @@ impl Host { /// and storage map to the state in the provided [`RollbackPoint`]. pub(super) fn pop_frame(&self, orp: Option) -> Result<(), HostError> { // Instance storage is tied to the frame and only exists in-memory. So - // instead of snapshotting it and rolling it back, we jsut flush the + // instead of snapshotting it and rolling it back, we just flush the // changes only when rollback is not needed. if orp.is_none() { self.flush_instance_storage()?; @@ -152,39 +147,31 @@ impl Host { self.try_borrow_context_mut()? .pop() .expect("unmatched host frame push/pop"); - // This is a bit hacky, as it relies on re-borrow to occur only doing - // the account contract invocations. Instead we should probably call it - // in more explicitly different fashion and check if we're calling it - // instead of a borrow check. - if let Ok(mut auth_manager) = self.0.authorization_manager.try_borrow_mut() { - auth_manager.pop_frame(); - } + self.try_borrow_authorization_manager()?.pop_frame(self)?; if self.try_borrow_context()?.is_empty() { // When there are no frames left, emulate authentication for the // recording auth mode. This is a no-op for the enforcing mode. - self.try_borrow_authorization_manager_mut()? + self.try_borrow_authorization_manager()? .maybe_emulate_authentication(self)?; - // Empty call stack in tests means that some contract function call - // has been finished and hence the authorization manager can be reset. - // In non-test scenarios, there should be no need to ever reset - // the authorization manager as the host instance shouldn't be - // shared between the contract invocations. - #[cfg(any(test, feature = "testutils"))] - { - *self.try_borrow_previous_authorization_manager_mut()? = - Some(self.try_borrow_authorization_manager()?.clone()); - self.try_borrow_authorization_manager_mut()?.reset(); - } } if let Some(rp) = orp { self.try_borrow_storage_mut()?.map = rp.storage; self.try_borrow_events_mut()?.rollback(rp.events)?; - if let Some(auth_rp) = rp.auth { - self.try_borrow_authorization_manager_mut()? - .rollback(auth_rp); - } + self.try_borrow_authorization_manager()? + .rollback(self, rp.auth)?; + } + // Empty call stack in tests means that some contract function call + // has been finished and hence the authorization manager can be reset. + // In non-test scenarios, there should be no need to ever reset + // the authorization manager as the host instance shouldn't be + // shared between the contract invocations. + #[cfg(any(test, feature = "testutils"))] + if self.try_borrow_context()?.is_empty() { + *self.try_borrow_previous_authorization_manager_mut()? = + Some(self.try_borrow_authorization_manager()?.clone()); + self.try_borrow_authorization_manager_mut()?.reset(); } Ok(()) } diff --git a/soroban-env-host/src/test/auth.rs b/soroban-env-host/src/test/auth.rs index a6bf76c0b..dfb734e43 100644 --- a/soroban-env-host/src/test/auth.rs +++ b/soroban-env-host/src/test/auth.rs @@ -2,12 +2,12 @@ use ed25519_dalek::Keypair; use rand::{thread_rng, Rng}; use soroban_env_common::xdr::{ AccountId, ContractDataDurability, HashIdPreimage, HashIdPreimageSorobanAuthorization, - PublicKey, ScAddress, ScNonceKey, ScSymbol, ScVal, ScVec, SorobanAddressCredentials, - SorobanAuthorizationEntry, SorobanAuthorizedContractFunction, SorobanAuthorizedFunction, - SorobanAuthorizedInvocation, SorobanCredentials, Uint256, + PublicKey, ScAddress, ScBytes, ScErrorCode, ScErrorType, ScNonceKey, ScSymbol, ScVal, ScVec, + SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedContractFunction, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, Uint256, VecM, }; use soroban_native_sdk_macros::contracttype; -use soroban_test_wasms::AUTH_TEST_CONTRACT; +use soroban_test_wasms::{AUTH_TEST_CONTRACT, DELEGATED_ACCOUNT_TEST_CONTRACT}; use crate::auth::RecordedAuthPayload; use crate::budget::AsBudget; @@ -85,7 +85,7 @@ impl SignNode { // This test uses `AuthContract::tree_fn` to build various authorization trees. impl AuthTest { - fn setup(signer_cnt: usize, contract_cnt: usize) -> Self { + fn setup_with_contract(signer_cnt: usize, contract_cnt: usize, contract_wasm: &[u8]) -> Self { let host = Host::test_host_with_recording_footprint(); // TODO: remove the `reset_unlimited` and instead reset inputs wherever appropriate // to respect the budget limit. @@ -118,7 +118,7 @@ impl AuthTest { } let mut contracts = vec![]; for _ in 0..contract_cnt { - let contract_id_obj = host.register_test_contract_wasm(AUTH_TEST_CONTRACT); + let contract_id_obj = host.register_test_contract_wasm(contract_wasm); contracts.push(contract_id_obj.try_into_val(&host).unwrap()); } Self { @@ -129,6 +129,10 @@ impl AuthTest { } } + fn setup(signer_cnt: usize, contract_cnt: usize) -> Self { + Self::setup_with_contract(signer_cnt, contract_cnt, AUTH_TEST_CONTRACT) + } + // Runs the test for the given setup in enforcing mode. `sign_payloads` // contain should contain a vec of `SignNode` inputs for every signer in the // setup. The nodes then are mapped to the respective `SorobanAuthorizationEntry` entries. @@ -1334,3 +1338,272 @@ fn test_invoker_subcontract_with_gaps() { false, ) } + +#[test] +fn test_require_auth_within_check_auth() { + let test = AuthTest::setup_with_contract(1, 2, DELEGATED_ACCOUNT_TEST_CONTRACT); + let auth_contract: Address = test + .host + .register_test_contract_wasm(AUTH_TEST_CONTRACT) + .try_into_val(&test.host) + .unwrap(); + + // Account 1 is used to authorize calls on behalf of account 0. + test.host + .call( + test.contracts[0].as_object(), + Symbol::try_from_small_str("init").unwrap(), + host_vec![&test.host, test.contracts[1]].as_object(), + ) + .unwrap(); + // Classic account is used to authorize calls on behalf of account 1. + test.host + .call( + test.contracts[1].as_object(), + Symbol::try_from_small_str("init").unwrap(), + host_vec![&test.host, test.key_to_address(&test.keys[0])].as_object(), + ) + .unwrap(); + let network_id: crate::xdr::Hash = test + .host + .with_ledger_info(|li: &LedgerInfo| Ok(li.network_id)) + .unwrap() + .try_into() + .unwrap(); + + let mut auth_entries = vec![]; + // Payload for account 0 is just the normal contract call payload. + let account_0_invocation = SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(SorobanAuthorizedContractFunction { + contract_address: auth_contract.to_sc_address().unwrap(), + function_name: "do_auth".try_into().unwrap(), + args: ScVec( + vec![ + ScVal::Address(test.contracts[0].to_sc_address().unwrap()), + ScVal::U32(123), + ] + .try_into() + .unwrap(), + ), + }), + sub_invocations: VecM::default(), + }; + let payload_preimage_for_account_0 = + HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: network_id.clone(), + invocation: account_0_invocation.clone(), + nonce: 1111, + signature_expiration_ledger: 1000, + }); + auth_entries.push(SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: test.contracts[0].to_sc_address().unwrap(), + nonce: 1111, + signature_args: ScVec(VecM::default()), + signature_expiration_ledger: 1000, + }), + root_invocation: account_0_invocation, + }); + let account_0_payload_hash = test + .host + .metered_hash_xdr(&payload_preimage_for_account_0) + .unwrap(); + + // Account 1 needs to authorize `__check_auth` for account 0 with its + // payload hash. + let account_1_invocation = SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(SorobanAuthorizedContractFunction { + contract_address: test.contracts[0].to_sc_address().unwrap(), + function_name: "__check_auth".try_into().unwrap(), + args: ScVec( + vec![ScVal::Bytes(ScBytes( + account_0_payload_hash.try_into().unwrap(), + ))] + .try_into() + .unwrap(), + ), + }), + sub_invocations: VecM::default(), + }; + let payload_preimage_for_account_1 = + HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: network_id.clone(), + invocation: account_1_invocation.clone(), + nonce: 2222, + signature_expiration_ledger: 2000, + }); + + auth_entries.push(SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: test.contracts[1].to_sc_address().unwrap(), + nonce: 2222, + signature_args: ScVec(VecM::default()), + signature_expiration_ledger: 2000, + }), + root_invocation: account_1_invocation, + }); + + let account_1_payload_hash = test + .host + .metered_hash_xdr(&payload_preimage_for_account_1) + .unwrap(); + // Classic account needs to authorize `__check_auth` for account 1 with its + // payload hash. + let classic_account_invocation = SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(SorobanAuthorizedContractFunction { + contract_address: test.contracts[1].to_sc_address().unwrap(), + function_name: "__check_auth".try_into().unwrap(), + args: ScVec( + vec![ScVal::Bytes(ScBytes( + account_1_payload_hash.try_into().unwrap(), + ))] + .try_into() + .unwrap(), + ), + }), + sub_invocations: VecM::default(), + }; + let payload_preimage_for_classic_account = + HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization { + network_id: network_id.clone(), + invocation: classic_account_invocation.clone(), + nonce: 3333, + signature_expiration_ledger: 3000, + }); + + let classic_account_payload_hash = test + .host + .metered_hash_xdr(&payload_preimage_for_classic_account) + .unwrap(); + + // Only one actual signature is needed (for the classic account). + let signature_args = host_vec![ + &test.host, + sign_payload_for_account(&test.host, &test.keys[0], &classic_account_payload_hash) + ]; + // Push an auth entry with bad nonce to fail auth at the deepest level. + auth_entries.push(SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: test.key_to_sc_address(&test.keys[0]), + nonce: 4444, + signature_args: test + .host + .call_args_to_scvec(signature_args.clone().into()) + .unwrap(), + signature_expiration_ledger: 3000, + }), + root_invocation: classic_account_invocation.clone(), + }); + + test.host + .set_authorization_entries(auth_entries.clone()) + .unwrap(); + let err = test + .host + .call( + auth_contract.as_object(), + Symbol::try_from_small_str("do_auth").unwrap(), + host_vec![&test.host, test.contracts[0], 123_u32].as_object(), + ) + .err() + .unwrap(); + assert!(err.error.is_type(ScErrorType::Crypto)); + assert!(err.error.is_code(ScErrorCode::InvalidInput)); + + // Add the correct entry - now the call should pass + auth_entries.pop(); + auth_entries.push(SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: test.key_to_sc_address(&test.keys[0]), + nonce: 3333, + signature_args: test.host.call_args_to_scvec(signature_args.into()).unwrap(), + signature_expiration_ledger: 3000, + }), + root_invocation: classic_account_invocation, + }); + + test.host.set_authorization_entries(auth_entries).unwrap(); + test.host + .call( + auth_contract.as_object(), + Symbol::try_from_small_str("do_auth").unwrap(), + host_vec![&test.host, test.contracts[0], 123_u32].as_object(), + ) + .unwrap(); + assert_eq!( + test.read_nonce_expiration(&test.contracts[0], 1111), + Some(1000) + ); + assert_eq!( + test.read_nonce_expiration(&test.contracts[1], 2222), + Some(2000) + ); + assert_eq!( + test.read_nonce_expiration( + &test + .key_to_address(&test.keys[0]) + .try_into_val(&test.host) + .unwrap(), + 3333 + ), + Some(3000) + ); +} + +#[test] +fn test_require_auth_for_self_within_check_auth() { + let test = AuthTest::setup_with_contract(1, 1, DELEGATED_ACCOUNT_TEST_CONTRACT); + let auth_contract: Address = test + .host + .register_test_contract_wasm(AUTH_TEST_CONTRACT) + .try_into_val(&test.host) + .unwrap(); + + // Use account as its own owner. + test.host + .call( + test.contracts[0].as_object(), + Symbol::try_from_small_str("init").unwrap(), + host_vec![&test.host, test.contracts[0]].as_object(), + ) + .unwrap(); + + let mut auth_entries = vec![]; + // Add a single auth entry. + auth_entries.push(SorobanAuthorizationEntry { + credentials: SorobanCredentials::Address(SorobanAddressCredentials { + address: test.contracts[0].to_sc_address().unwrap(), + nonce: 1111, + signature_args: ScVec(VecM::default()), + signature_expiration_ledger: 1000, + }), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(SorobanAuthorizedContractFunction { + contract_address: auth_contract.to_sc_address().unwrap(), + function_name: "do_auth".try_into().unwrap(), + args: ScVec( + vec![ + ScVal::Address(test.contracts[0].to_sc_address().unwrap()), + ScVal::U32(123), + ] + .try_into() + .unwrap(), + ), + }), + sub_invocations: VecM::default(), + }, + }); + let err = test + .host + .call( + auth_contract.as_object(), + Symbol::try_from_small_str("do_auth").unwrap(), + host_vec![&test.host, test.contracts[0], 123_u32].as_object(), + ) + .err() + .unwrap(); + // Make sure we're just getting an auth error and not ending up in some + // context/recursion error states. + assert!(err.error.is_type(ScErrorType::Auth)); + assert!(err.error.is_code(ScErrorCode::InvalidAction)); +} diff --git a/soroban-env-host/src/test/storage.rs b/soroban-env-host/src/test/storage.rs index 377cda401..cd2b0699d 100644 --- a/soroban-env-host/src/test/storage.rs +++ b/soroban-env-host/src/test/storage.rs @@ -29,7 +29,7 @@ fn test_storage(host: &Host, contract_id: AddressObject, storage: &str) { host.call( contract_id, storage_fn_name(host, "put", storage), - host_vec![host, key_1, 1234_u64, ()].into(), + host_vec![host, key_1, 1234_u64].into(), ) .unwrap(); @@ -61,11 +61,11 @@ fn test_storage(host: &Host, contract_id: AddressObject, storage: &str) { ) .unwrap(); - // Put anothrer key and verify it's there + // Put another key and verify it's there host.call( contract_id, storage_fn_name(host, "put", storage), - host_vec![host, key_2, u64::MAX, ()].into(), + host_vec![host, key_2, u64::MAX].into(), ) .unwrap(); assert_eq!( @@ -124,7 +124,7 @@ fn test_storage(host: &Host, contract_id: AddressObject, storage: &str) { host.call( contract_id, storage_fn_name(host, "put", storage), - host_vec![host, key_2, 4321_u64, ()].into(), + host_vec![host, key_2, 4321_u64].into(), ) .unwrap(); assert_eq!( diff --git a/soroban-env-host/src/test/token.rs b/soroban-env-host/src/test/token.rs index 1885d3842..63966fa6d 100644 --- a/soroban-env-host/src/test/token.rs +++ b/soroban-env-host/src/test/token.rs @@ -25,9 +25,9 @@ use soroban_env_common::{ SorobanAuthorizedFunction, SorobanAuthorizedInvocation, }, xdr::{ - AccountId, AlphaNum12, AlphaNum4, Asset, AssetCode12, AssetCode4, Hash, HostFunctionType, - LedgerEntryData, LedgerKey, Liabilities, PublicKey, ScErrorCode, ScErrorType, - TrustLineEntry, TrustLineEntryExt, TrustLineEntryV1, TrustLineEntryV1Ext, TrustLineFlags, + AccountId, AlphaNum12, AlphaNum4, Asset, AssetCode12, AssetCode4, Hash, LedgerEntryData, + LedgerKey, Liabilities, PublicKey, ScErrorCode, ScErrorType, TrustLineEntry, + TrustLineEntryExt, TrustLineEntryV1, TrustLineEntryV1Ext, TrustLineFlags, }, EnvBase, Val, }; @@ -286,22 +286,18 @@ impl TokenTest { ) } - fn run_from_account(&self, account_id: AccountId, f: F) -> Result + fn run_from_account(&self, account_id: AccountId, f: F) -> Result where T: Into, F: FnOnce() -> Result, { + let prev_source_account = self.host.source_account_id()?; self.host.set_source_account(account_id)?; - self.host.with_frame( - Frame::HostFunction(HostFunctionType::InvokeContract), - || { - let res = f(); - match res { - Ok(v) => Ok(v.into()), - Err(e) => Err(e), - } - }, - ) + let res = f(); + if let Some(acc) = prev_source_account { + self.host.set_source_account(acc)?; + } + res } } diff --git a/soroban-test-wasms/src/lib.rs b/soroban-test-wasms/src/lib.rs index b76563fb4..11f9cb53a 100644 --- a/soroban-test-wasms/src/lib.rs +++ b/soroban-test-wasms/src/lib.rs @@ -64,3 +64,5 @@ pub const AUTH_TEST_CONTRACT: &[u8] = include_bytes!("../wasm-workspace/opt/auth_test_contract.wasm").as_slice(); pub const UPDATEABLE_CONTRACT: &[u8] = include_bytes!("../wasm-workspace/opt/example_updateable_contract.wasm").as_slice(); +pub const DELEGATED_ACCOUNT_TEST_CONTRACT: &[u8] = + include_bytes!("../wasm-workspace/opt/test_delegated_account.wasm").as_slice(); diff --git a/soroban-test-wasms/wasm-workspace/Cargo.lock b/soroban-test-wasms/wasm-workspace/Cargo.lock index f3d2de8d6..e9e081e28 100644 --- a/soroban-test-wasms/wasm-workspace/Cargo.lock +++ b/soroban-test-wasms/wasm-workspace/Cargo.lock @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "crate-git-revision" -version = "0.0.4" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f998aef136a4e7833b0e4f0fc0939a59c40140b28e0ffbf524ad84fb2cc568c8" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" dependencies = [ "serde", "serde_derive", @@ -1039,7 +1039,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "soroban-env-common" -version = "0.0.16" +version = "0.0.17" dependencies = [ "arbitrary", "crate-git-revision", @@ -1056,7 +1056,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "0.0.16" +version = "0.0.17" dependencies = [ "soroban-env-common", "static_assertions", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "0.0.16" +version = "0.0.17" dependencies = [ "backtrace", "curve25519-dalek", @@ -1089,7 +1089,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "0.0.16" +version = "0.0.17" dependencies = [ "itertools", "proc-macro2", @@ -1103,7 +1103,7 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" -version = "0.8.4" +version = "0.9.1" dependencies = [ "serde", "serde_json", @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "soroban-native-sdk-macros" -version = "0.0.16" +version = "0.0.17" dependencies = [ "itertools", "proc-macro2", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "0.8.4" +version = "0.9.1" dependencies = [ "arbitrary", "bytes-lit", @@ -1140,7 +1140,7 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "0.8.4" +version = "0.9.1" dependencies = [ "crate-git-revision", "darling", @@ -1158,7 +1158,7 @@ dependencies = [ [[package]] name = "soroban-spec" -version = "0.8.4" +version = "0.9.1" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "0.8.4" +version = "0.9.1" dependencies = [ "prettyplease", "proc-macro2", @@ -1183,27 +1183,16 @@ dependencies = [ [[package]] name = "soroban-wasmi" version = "0.30.0-soroban" -source = "git+https://github.com/stellar/wasmi?rev=1a2bc7f#1a2bc7f3801c565c2dab22021255a164c05a7f76" +source = "git+https://github.com/stellar/wasmi?rev=3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995#3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995" dependencies = [ "intx", "smallvec", - "soroban-wasmi_core", "spin", "wasmi_arena", + "wasmi_core", "wasmparser-nostd", ] -[[package]] -name = "soroban-wasmi_core" -version = "0.30.0-soroban" -source = "git+https://github.com/stellar/wasmi?rev=1a2bc7f#1a2bc7f3801c565c2dab22021255a164c05a7f76" -dependencies = [ - "downcast-rs", - "libm", - "num-traits", - "paste", -] - [[package]] name = "spin" version = "0.9.8" @@ -1237,8 +1226,8 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "0.0.16" -source = "git+https://github.com/stellar/rs-stellar-xdr?rev=6750a11325a7c4eed1e79372d136106e5bfecaff#6750a11325a7c4eed1e79372d136106e5bfecaff" +version = "0.0.17" +source = "git+https://github.com/stellar/rs-stellar-xdr?rev=0f16673441898162c9996da6117be2280ef8fd84#0f16673441898162c9996da6117be2280ef8fd84" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1282,6 +1271,13 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "test_delegated_account" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -1416,7 +1412,18 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasmi_arena" version = "0.4.0" -source = "git+https://github.com/stellar/wasmi?rev=1a2bc7f#1a2bc7f3801c565c2dab22021255a164c05a7f76" +source = "git+https://github.com/stellar/wasmi?rev=3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995#3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995" + +[[package]] +name = "wasmi_core" +version = "0.12.0" +source = "git+https://github.com/stellar/wasmi?rev=3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995#3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995" +dependencies = [ + "downcast-rs", + "libm", + "num-traits", + "paste", +] [[package]] name = "wasmparser" diff --git a/soroban-test-wasms/wasm-workspace/Cargo.toml b/soroban-test-wasms/wasm-workspace/Cargo.toml index 727d50202..be58d2b52 100644 --- a/soroban-test-wasms/wasm-workspace/Cargo.toml +++ b/soroban-test-wasms/wasm-workspace/Cargo.toml @@ -30,7 +30,8 @@ members = [ "complex", "fannkuch", "simple_account", - "update" + "update", + "delegated_account" ] [profile.release] opt-level = "z" @@ -51,7 +52,7 @@ soroban-env-guest = { path = "../../soroban-env-guest" } soroban-env-host = { path = "../../soroban-env-host" } [workspace.dependencies.soroban-sdk] -version = "0.8.0" +version = "0.9.1" git = "https://github.com/stellar/rs-soroban-sdk" # Always build using the local SDK. Usually the env change is accompanied with diff --git a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs index 5cb36e764..2feebcfd7 100644 --- a/soroban-test-wasms/wasm-workspace/auth/src/lib.rs +++ b/soroban-test-wasms/wasm-workspace/auth/src/lib.rs @@ -65,6 +65,17 @@ impl AuthContract { (vec![&env, curr_address], tree.clone()).into_val(&env), ); } + + // Just authorize the call tree, but don't call anything. + pub fn invoker_auth_fn_no_call(env: Env, tree: TreeNode) { + // Build auth entries from children - tree root auth works + // automatically. + let mut auth_entries = vec![&env]; + for child in tree.children.iter() { + auth_entries.push_back(tree_to_invoker_contract_auth(&env, &child)); + } + env.authorize_as_current_contract(auth_entries); + } } // Converts the whole provided tree to invoker auth entries, ignoring diff --git a/soroban-test-wasms/wasm-workspace/complex/src/lib.rs b/soroban-test-wasms/wasm-workspace/complex/src/lib.rs index 83111853d..67c7859f5 100644 --- a/soroban-test-wasms/wasm-workspace/complex/src/lib.rs +++ b/soroban-test-wasms/wasm-workspace/complex/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, contracttype, Bytes, BytesN, Env, symbol_short, Vec}; +use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Bytes, BytesN, Env, Vec}; // This is a "complex" contract that uses a nontrivial amount of the host // interface from the guest: UDTs (thus maps), vectors, byte arrays and linear @@ -40,6 +40,6 @@ impl Contract { e.events().publish((data.clone(),), hash); e.logs() .add("vec with half hash", &[vec_with_half_hash.to_val()]); - e.storage().temporary().set(&data, &my_ledger, None); + e.storage().temporary().set(&data, &my_ledger); } } diff --git a/soroban-test-wasms/wasm-workspace/contract_data/src/lib.rs b/soroban-test-wasms/wasm-workspace/contract_data/src/lib.rs index 123f08623..f53097114 100644 --- a/soroban-test-wasms/wasm-workspace/contract_data/src/lib.rs +++ b/soroban-test-wasms/wasm-workspace/contract_data/src/lib.rs @@ -6,8 +6,8 @@ pub struct Contract; #[contractimpl] impl Contract { - pub fn put_persistent(e: Env, key: Symbol, val: u64, flags: Option) { - e.storage().persistent().set(&key, &val, flags); + pub fn put_persistent(e: Env, key: Symbol, val: u64) { + e.storage().persistent().set(&key, &val); } pub fn del_persistent(e: Env, key: Symbol) { @@ -26,8 +26,8 @@ impl Contract { e.storage().persistent().bump(&key, min_to_live) } - pub fn put_temporary(e: Env, key: Symbol, val: u64, flags: Option) { - e.storage().temporary().set(&key, &val, flags) + pub fn put_temporary(e: Env, key: Symbol, val: u64) { + e.storage().temporary().set(&key, &val) } pub fn del_temporary(e: Env, key: Symbol) { @@ -46,8 +46,8 @@ impl Contract { e.storage().temporary().bump(&key, min_to_live) } - pub fn put_instance(e: Env, key: Symbol, val: u64, flags: Option) { - e.storage().instance().set(&key, &val, flags) + pub fn put_instance(e: Env, key: Symbol, val: u64) { + e.storage().instance().set(&key, &val) } pub fn del_instance(e: Env, key: Symbol) { diff --git a/soroban-test-wasms/wasm-workspace/delegated_account/Cargo.toml b/soroban-test-wasms/wasm-workspace/delegated_account/Cargo.toml new file mode 100644 index 000000000..befef45ae --- /dev/null +++ b/soroban-test-wasms/wasm-workspace/delegated_account/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test_delegated_account" +version = "0.0.0" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +edition = "2021" +rust-version = "1.65" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } diff --git a/soroban-test-wasms/wasm-workspace/delegated_account/src/lib.rs b/soroban-test-wasms/wasm-workspace/delegated_account/src/lib.rs new file mode 100644 index 000000000..32db3b4ce --- /dev/null +++ b/soroban-test-wasms/wasm-workspace/delegated_account/src/lib.rs @@ -0,0 +1,37 @@ +#![no_std] +use soroban_sdk::{ + auth::Context, contract, contractimpl, contracttype, Address, BytesN, Env, IntoVal, Val, Vec, +}; + +#[contract] +struct DelegatedAccount; + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Owner, +} + +#[contractimpl] +impl DelegatedAccount { + pub fn init(env: Env, owner: Address) { + env.storage().persistent().set(&DataKey::Owner, &owner); + } + + #[allow(non_snake_case)] + pub fn __check_auth( + env: Env, + signature_payload: BytesN<32>, + signature_args: Vec, + _auth_context: Vec, + ) { + if signature_args.len() != 0 { + panic!("incorrect number of signature args"); + } + env.storage() + .persistent() + .get::<_, Address>(&DataKey::Owner) + .unwrap() + .require_auth_for_args((signature_payload,).into_val(&env)); + } +} diff --git a/soroban-test-wasms/wasm-workspace/opt/auth_test_contract.wasm b/soroban-test-wasms/wasm-workspace/opt/auth_test_contract.wasm index dacd4b604f08533fa199343e6c18a5661b0f15c6..e8d421cb328ddabc51a41a10482ff461aa615be4 100644 GIT binary patch delta 879 zcmZ`%O=}ZT6utMoq)9VL^U?~9P1?RpQf-vjNs}~9LC6S-cBKfq(&?B?XeVtdZ5mW) zve1R#rsaVyEwYa29%!PAD4_cv@}aOH8W_+rg9S9b3T(HOc!F`NS^~j`EM`N=EmU~Xg)?)XA;d1;F>5V)a#+ky>aq){;mhw zJ|gd>!90Mbjv0_Bq0EECsWu6Mu86U!#4N8~y%?da`32l8hZLC?nDCdmpjh0MAR=VY zoD}24KdO$qg3?s0lNL83=!wyj@XEyI5aS3M5$C=8h1avpz5a&h2akPEH_A*Ow$wR2 z-@zrEcEvF5ik^NnL6EdjL6?i8%PAtJD0=|M8Z9DvcdAe{gBdjwZS_A!TR2l>*uyR* zL{n#UKglKHhf`BrlAGm0PY;mX+mES*SdPkMI-V`|!E{Y-9(g;otY)ADSqruAPv- E0gJnO=}ZT6n*Etnb^!sn{isue6*c>NURc>Hko|Xg^h@nZe56lI+-vN>_DT~5_Dmr zq9V93;UR)pP!Ly27xO0sS1$Yu`U3=U;ldknbMATP-gDo%k1L-zhl;M$vfT0TS;r{p0%pdc^t zF;2>Fd>4!If{01^DJ~PPD_cm*C(1jVkY)7-DNog@@>RVmN~Rr`6MGza`Euqyt(s3B zm2w7j-RAALh)V1tPw9MvZ?s`6HeV5rElhIB@5z;Usts|MCe4W8H;c?ASw-6DdYP&DqDyWqH=9Q17}?Of8ek`TqygU&-gj5jQgN`Pl(7`G#=*-8_%tiTS?n)v@?=d9I8j7-R_N8SlOz*So_`TVO+_cKnxMhvvU)Cekn&)c8 yu;m2ZRcSI1M_4v<1{m)L`x&{ L)HKVcSI1M_4v<1{m)L`x&{ L)HKV<$C!fwza|gq delta 56 zcmbQqF_UA%EM^@GJrm_rOM{do%QOoE14|cSI1M_4v<1{m)L`x&{ L)HKV`=uK2Z;%`gnrfR>Fs%lc%Oga&FUIR5V}NBlWfHD zYxYwy)JkH0KwI_)$iK6x@?=d2hDn23AW5P}mJ0BwEh#V)g?1t~*icf3%-0q5 zrbDu>BG8JXl?bg^Y5yltN2Qa;4X$*&&X_4~GHY=B`H`uY8 zQO^&ZNzZXZdtyhn<+`!u#D3&j-lXru{cbqudQm*sK0G?RxPoc^uI=XgRWld5nE84Q O7sIWgEr$G~`uPXD(@vZK delta 527 zcmZ{gy-piZ5QS&%k9{$qETVA&NV8xmSSVz%cfGbm;oUp|Jb{huMcFt_pa{)g2}e;n zgth~vrb=P?0rCRr(nO*FiH298N@gt-Dw^-i%(S?#$SjF$BSrHh^?V%Tig2V)=i7HgXP8Dnt~M zi!S1?{2Irv)_}m7BXkx>kopnR*I0dt1<93sByP|xtBEP9ia#?s?8 z=ISqwW3|6n4C&jITZ(YrEps4!Sl2bElP(e+7EUyzi={epQj%1rl2hS^(&A&&JN;Ss z?QMT!*zpDjouut~okrUWc3RCa7$mKO=5XjY`>h~Jf`%W2;hVwxPsbm{9x`iDn+UOx6cLrrhYGT-(YbGh>3Su=k(|K}g!-(uAO diff --git a/soroban-test-wasms/wasm-workspace/opt/example_create_contract.wasm b/soroban-test-wasms/wasm-workspace/opt/example_create_contract.wasm index 3a98cfbee13cb04beb379c54e039e15ddc2733c5..178be909c5031f31ff3b47aac42265d7fab48ed5 100644 GIT binary patch delta 56 zcmX@XdV+O>C6kV&o}qH8iAADuvWc;2qG7UOs-c0YX_|p?nnkLqfqAl-ahjP?qNR~} LYMSNbRHkGAy?PIg delta 56 zcmX@XdV+O>C6kVYo{4g*r9n!PWtxS7fu)g!fvKUHvAJnVnpu)@YO0BGqM2!0nyHbA Lsk!;&RHkGAxWx}a diff --git a/soroban-test-wasms/wasm-workspace/opt/example_fannkuch.wasm b/soroban-test-wasms/wasm-workspace/opt/example_fannkuch.wasm index 5a589cfa3860bc81064b52fbbafbc8511329f0d0..9f55bb69a44544736ca1aa4fc7c45514cd97817a 100644 GIT binary patch delta 56 zcmbQjF@cSI1M_4v<1{m)L`x&{ L)HKVe}R9)4>lc3JwxSG6N^OSWD{f4M8jmmR6_$((=-F)G>cSI1M_4v<1{m)L`x&{ L)HKVxU45 delta 56 zcmcb>e}R9)4>lbOJrm_rOM{do%QOoE14|cSI1M_4v<1{m)L`x&{ L)HKVcSI1M_4v<1{m)L`x&{ M)HKV@r`UWL0ncSI1M_4v<1{m)L`x&{ L)HKV<+)Tj$=0p$j delta 56 zcmcb|a*t)heMTJ%Jrm_rOM{do%QOoE14|vfV(0 zYDXffSh8Tj5>{Dd%Nll-5K{jEWs3xVfCYT#j-9raJagupbHDSQ*9_&u13^ThQQ4B? zu^4X&zUVQ2xD)|X|v`cfqi{{$x?R=ECi~hC}QM) zHxp=?>~{!OlavJZ`>EuYUUHdaOSV+YXyf12Ekl+zBpEVc7Cz4wPMa#Ms>ofpE0Unaj~gtEZJIjMFGX2*ru1F6QC=sDYY{c<^(r)91bo*3`exF8_ze@BBVt= zl2kHIFB4G`0=~jTgaIG+HJl$}17}`DNEtHBwhn#tnGlnU`M?=)T7(Vd5rB@X&iPEY z_!!GEILCb2QO!gqW3@B|%VhMkCw=S(5MPm-G~+Skrty$*qB2URL#TPYpxtEpX(jtl ze3QMPhq{%Cq{7A5Wfqm#j~A@Dcb-&?2JZw!6hnr%Ax}d4xeW}qqH1SGIH=#~;b7i1 zrH+H1LDxS=w3pW~lMTB`ShfZfAYgSB zI;k%5a78G`O>ui&mmr}?ST#4CnEhFe65mpy+}j4u2_a==!+UOlc=NiYD|AR7?0B^? z*^1XbMpV5ax#rj!%};Pk171UAt6ft{#y3IFs-qN6i|lIxqn$$W)BS53@~TD0YH~RR zL`|AB=%qf}KDm}C6IeCEBMfmj!O^sV#BgVVI z?}BOWJm@Ie3)g(Ea~uatP`n}-GI?8XRP>Jyicx-qPlWqj z#dVFoUYOb6GJ1pIkwbygLSwagYqfr}zxQ}|w0~Tl#x)!ahugi)c*j&l{aa(WtMl@6^|JHjDLov$L_bvvDiGbE{eG+_|~EHy9n$zv3eL@JZ2e)MjaL znBYTvX6_d*q_~Jnf=!Ocv47GC@0gU3VGs=uZ-zRkN2Wt*#7d^K_gwg)dE1|Phk{U3BdMiKx3 literal 0 HcmV?d00001 diff --git a/soroban-test-wasms/wasm-workspace/simple_account/src/lib.rs b/soroban-test-wasms/wasm-workspace/simple_account/src/lib.rs index 0d7439386..bff539f1b 100644 --- a/soroban-test-wasms/wasm-workspace/simple_account/src/lib.rs +++ b/soroban-test-wasms/wasm-workspace/simple_account/src/lib.rs @@ -13,16 +13,12 @@ pub enum DataKey { #[contractimpl] impl SimpleAccount { pub fn init(env: Env, public_key: BytesN<32>) { - env.storage() - .persistent() - .set(&DataKey::Owner, &public_key, None); + env.storage().persistent().set(&DataKey::Owner, &public_key); } pub fn set_owner(env: Env, new_owner: BytesN<32>) { env.current_contract_address().require_auth(); - env.storage() - .persistent() - .set(&DataKey::Owner, &new_owner, None); + env.storage().persistent().set(&DataKey::Owner, &new_owner); } #[allow(non_snake_case)]