soroban_env_host/auth.rs
1//! # Auth conceptual overview
2//!
3//! This module is responsible for two separate tasks (both starting with
4//! "auth"):
5//!
6//! - Authorization: deciding if some action should be allowed by some policy.
7//! - Authentication: deciding if some credential or signature is authentic.
8//!
9//! As one would expect, authorization can (though doesn't always) depend on
10//! authentication: part of judging whether some action is allowed may depend on
11//! someone presenting a signed credential, at which point one must evaluate the
12//! credential's authenticity.
13//!
14//! Moreover this subsystem is responsible (as will be discussed in detail
15//! below) with facilitating two different _directions_ for each of these tasks:
16//!
17//! - Contracts can _require_ auth services provided by this module.
18//! - Contracts can _provide_ auth services required by this module.
19//!
20//! And again, in both directions the "auth services" required or provided may
21//! be _either_ of authorization, authentication, or both.
22//!
23//! All auth services reason about invocations and authorizations, so we
24//! next turn our attention to these.
25//!
26//! ## Invocations
27//!
28//! A Soroban transaction can be seen as a tree of _invocations_: these are
29//! usually invocations of contract functions, but may also be other host
30//! functions requiring authorization, such as the 'create contract' function.
31//!
32//! The "invocation tree" corresponds to the fact that each invocation may cause
33//! sub-invocations, each of which may have sub-sub-invocations, and so on.
34//! Contracts often invoke other contracts.
35//!
36//! Each invocation in the tree is mediated by the Soroban host, and typically
37//! represents a transition between trust domains (as different contracts are
38//! written by different authors), so invocations are the natural boundary at
39//! which to evaluate authorization.
40//!
41//! In other words: authorization happens _in terms of_ invocations; they are
42//! the conceptual units for which authorization is granted or denied.
43//!
44//! Note that invocations are _not_ function calls within a contract's own WASM
45//! bytecode. The host can't see a call from one WASM function to another inside
46//! a single WASM blob, and in general it does not concern itself with authorizing
47//! those. Invocations are bigger: what are often called "cross-contract calls",
48//! that transfer control from one WASM VM to another.
49//!
50//! ## Authorized Invocations
51//!
52//! Each invocation may -- usually early on -- call the host function
53//! `require_auth(Address)`: this is the main entrypoint to the auth module.
54//!
55//! The `require_auth` function takes an `Address` that the contract provides,
56//! that identifies some abstract entity responsible for _authorizing the
57//! current invocation_. The contract calling `require_auth` must therefore
58//! somehow select (directly or indirectly, perhaps from its own internal
59//! configuration or from some argument it was passed associated with the
60//! operation it's performing) _which entity_ it wishes to predicate its
61//! execution on the authorization of. As we'll see, there are multiple ways
62//! this entity may provide authorization. It may also require authorization
63//! from multiple entities!
64//!
65//! (There is also a secondary entrypoint called `require_auth_for_args` that
66//! allows customizing the invocation being authorized, in case the current
67//! contract invocation -- function name and argument list -- isn't quite the
68//! one desired, but this distinction is unimportant in this discussion.)
69//!
70//! For a given `Address`, the auth module maintains one or more tree-shaped
71//! data structures called the `AuthorizedInvocation`s of the `Address`, which
72//! are incrementally matched by each invocation's calls to
73//! `require_auth(Address)`.
74//!
75//! Each such tree essentially represents a _pattern_ of invocations the
76//! `Address` authorizes, that the _actual_ execution context of a running tree
77//! of contract invocations needs to match when it calls `require_auth`. Any
78//! pattern node that matches an invocation is then permanently _associated_
79//! with the actual invocation it matched, such that sub-patterns can only match
80//! at actual sub-invocations, allowing the authorizing party to globally
81//! restrict the _contexts_ in which a sub-invocation may match.
82//!
83//! Furthermore each pattern node is permanently invalidated as it matches so
84//! that it can never match more than once per transaction. If a user wishes to
85//! authorize two instances of the same pattern within a transaction, they must
86//! provide two separate copies.
87//!
88//! # Addresses
89//!
90//! As described above, `AuthorizedInvocation`s define the trees of invocations
91//! that are authorized by some `Address`. But what is an Address? Concretely it
92//! is either a Stellar `AccountID` or the `Hash` identity of some contract. But
93//! _conceptually_ the Address used to authorize an invocation may be one of 4
94//! different types.
95//!
96//! 1. The address of a contract that is an _invoker_. We say that if contract
97//! C invokes contract D, then C authorized D. This is simple and requires
98//! no credentials as the host literally observes the call from C to D. It
99//! is a slight conceptual stretch but makes sense: if C didn't want to
100//! authorize D, it wouldn't have invoked it! Further invoker-contract
101//! authorizations for _indirect_ calls (C calls D calls E, C wants to
102//! authorize sub-calls to E) can also be provided on the fly by contracts
103//! calling `authorize_as_curr_contract`, passing a vector of the
104//! Val-encoded type `InvokerContractAuthEntry`.
105//!
106//! 2. The address of a Stellar classic account, identified by `AccountID`,
107//! that must supply `SorobanAddressCredentials` for any
108//! `AuthorizedInvocation` it authorizes, satisfying the account's classic
109//! multisig authorization to its medium threshold.
110//!
111//! 3. The address of a Stellar classic account that happens to be the
112//! _transaction source account_. In this case we assume the transaction
113//! signatures already met the requirements of the account before the
114//! Soroban host was even instantiated, and so the `AuthorizedInvocation`
115//! for such an address can be accompanied by the constant credential
116//! `SOROBAN_CREDENTIALS_SOURCE_ACCOUNT` that's considered authentic by
117//! assumption.
118//!
119//! 4. The address of a contract that is a _custom account_. In this case the
120//! `AuthorizedInvocation` is still accompanied by
121//! `SorobanAddressCredentials` but _interpreting_ those credentials (and
122//! indeed interpreting the entire authorization request) is delegated to a
123//! contract. The contract must export a function called `__check_auth` and
124//! it will be passed the abstract, uninterpreted `Val` from the
125//! credential's "signature" field, along with a hash of the material it
126//! expects the signature to authenticate, and a structured summary of the
127//! auth context. The `__check_auth` function may potentially re-enter the
128//! auth module by calling `require_auth` on some other `Address`.
129//!
130//! Each of these 4 forms of address may be passed to `require_auth`, which will
131//! then serve as a key to look up an `AuthorizedInvocation` to match against
132//! the invocation being authorized, and potentially perform further
133//! authentication or custom-auth logic.
134//!
135//! The first type -- contract invoker address -- is associated with a set of
136//! `AuthorizedInvocation`s that is dynamic, evolves during execution of the
137//! transaction, and requires no credentials. The other 3 types are static, are
138//! provided as input to the transaction, and carry credentials that may require
139//! authentication. Therefore the first type and the latter 3 types are tracked
140//! in different data structures. But this is merely an implementation detail;
141//! addresses in all 4 conceptual roles can be passed to `require_auth` without
142//! any concern for which kind fulfils the requirement at runtime.
143//!
144//! In the cases with nontrivial `SorobanAddressCredentials` (2 and 4), the auth
145//! module takes care of evaluating signature expiration times and recording
146//! nonces to the ledger automatically, to prevent replay.
147//!
148use std::cell::RefCell;
149use std::rc::Rc;
150
151use crate::{
152 budget::{AsBudget, Budget},
153 builtin_contracts::{
154 account_contract::{check_account_authentication, check_account_contract_auth},
155 invoker_contract_auth::invoker_contract_auth_to_authorized_invocation,
156 },
157 host::{
158 metered_clone::{MeteredAlloc, MeteredClone, MeteredContainer, MeteredIterator},
159 metered_hash::{CountingHasher, MeteredHash},
160 Frame,
161 },
162 host_object::HostVec,
163 xdr::{
164 ContractDataEntry, CreateContractArgsV2, HashIdPreimage,
165 HashIdPreimageSorobanAuthorization, InvokeContractArgs, LedgerEntry, LedgerEntryData,
166 LedgerEntryExt, ScAddress, ScErrorCode, ScErrorType, ScNonceKey, ScVal,
167 SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanCredentials,
168 },
169 AddressObject, Compare, Host, HostError, Symbol, TryFromVal, TryIntoVal, Val, VecObject,
170};
171
172use super::xdr;
173use super::xdr::Hash;
174
175#[cfg(any(test, feature = "recording_mode"))]
176use crate::{
177 builtin_contracts::{account_contract::AccountEd25519Signature, base_types::BytesN},
178 host::error::TryBorrowOrErr,
179 xdr::{ContractExecutable, PublicKey},
180};
181#[cfg(any(test, feature = "recording_mode"))]
182use rand::Rng;
183#[cfg(any(test, feature = "recording_mode"))]
184use std::collections::BTreeMap;
185
186// Authorization manager encapsulates host-based authentication & authorization
187// framework.
188// This supports enforcing authentication & authorization of the contract
189// invocation trees as well as recording the authorization requirements in
190// simulated environments (such as tests or preflight).
191#[derive(Clone)]
192pub struct AuthorizationManager {
193 // Mode of operation of this AuthorizationManager. This can't be changed; in
194 // order to switch the mode a new instance of AuthorizationManager has to
195 // be created.
196 mode: AuthorizationMode,
197 // Per-address trackers of authorized invocations.
198 // Every tracker takes care about a single rooted invocation tree for some
199 // address. There can be multiple trackers per address.
200 // The internal structure of this field is build in such a way that trackers
201 // can be borrowed mutably independently, while still allowing for
202 // modification of the `account_trackers` vec itself.
203 account_trackers: RefCell<Vec<RefCell<AccountAuthorizationTracker>>>,
204 // Per-address trackers for authorization performed by the contracts at
205 // execution time (as opposed to signature-based authorization for accounts).
206 // Contract authorizations are always enforced independently of the `mode`,
207 // as they are self-contained and fully defined by the contract logic.
208 invoker_contract_trackers: RefCell<Vec<InvokerContractAuthorizationTracker>>,
209 // Call stack of relevant host function and contract invocations, moves mostly
210 // in lock step with context stack in the host.
211 call_stack: RefCell<Vec<AuthStackFrame>>,
212}
213
214macro_rules! impl_checked_borrow_helpers {
215 ($field:ident, $t:ty, $borrow:ident, $borrow_mut:ident) => {
216 impl AuthorizationManager {
217 #[allow(dead_code)]
218 fn $borrow(&self, host: &Host) -> Result<std::cell::Ref<'_, $t>, HostError> {
219 use crate::host::error::TryBorrowOrErr;
220 self.$field.try_borrow_or_err_with(
221 host,
222 concat!(
223 "authorization_manager.",
224 stringify!($field),
225 ".try_borrow failed"
226 ),
227 )
228 }
229
230 #[allow(dead_code)]
231 fn $borrow_mut(&self, host: &Host) -> Result<std::cell::RefMut<'_, $t>, HostError> {
232 use crate::host::error::TryBorrowOrErr;
233 self.$field.try_borrow_mut_or_err_with(
234 host,
235 concat!(
236 "authorization_manager.",
237 stringify!($field),
238 ".try_borrow_mut failed"
239 ),
240 )
241 }
242 }
243 };
244}
245
246impl_checked_borrow_helpers!(
247 account_trackers,
248 Vec<RefCell<AccountAuthorizationTracker>>,
249 try_borrow_account_trackers,
250 try_borrow_account_trackers_mut
251);
252
253impl_checked_borrow_helpers!(
254 invoker_contract_trackers,
255 Vec<InvokerContractAuthorizationTracker>,
256 try_borrow_invoker_contract_trackers,
257 try_borrow_invoker_contract_trackers_mut
258);
259
260impl_checked_borrow_helpers!(
261 call_stack,
262 Vec<AuthStackFrame>,
263 try_borrow_call_stack,
264 try_borrow_call_stack_mut
265);
266
267// The authorization payload recorded for an address in the recording
268// authorization mode.
269#[cfg(any(test, feature = "recording_mode"))]
270#[derive(Debug)]
271pub struct RecordedAuthPayload {
272 pub address: Option<ScAddress>,
273 pub nonce: Option<i64>,
274 pub invocation: xdr::SorobanAuthorizedInvocation,
275}
276
277// Snapshot of `AuthorizationManager` to use when performing the callstack
278// rollbacks.
279pub struct AuthorizationManagerSnapshot {
280 account_trackers_snapshot: AccountTrackersSnapshot,
281 invoker_contract_tracker_root_snapshots: Vec<AuthorizedInvocationSnapshot>,
282 #[cfg(any(test, feature = "recording_mode"))]
283 tracker_by_address_handle: Option<BTreeMap<u32, usize>>,
284}
285
286// Snapshot of the `account_trackers` in `AuthorizationManager`.
287enum AccountTrackersSnapshot {
288 // In enforcing mode we only need to snapshot the mutable part of the
289 // trackers.
290 // `None` means that the tracker is currently in authentication process and
291 // shouldn't be modified (as the tracker can't be used to authenticate
292 // itself).
293 Enforcing(Vec<Option<AccountAuthorizationTrackerSnapshot>>),
294 // In recording mode snapshot the whole vector, as we create trackers
295 // lazily and hence the outer vector itself might change.
296 #[cfg(any(test, feature = "recording_mode"))]
297 Recording(Vec<RefCell<AccountAuthorizationTracker>>),
298}
299
300// Additional AuthorizationManager fields needed only for the recording mode.
301#[cfg(any(test, feature = "recording_mode"))]
302#[derive(Clone)]
303struct RecordingAuthInfo {
304 // Maps the `Address` object identifiers to the respective tracker indices
305 // in `trackers`
306 // This allows to disambiguate between the addresses that have the same
307 // value, but are specified as two different objects (e.g. as two different
308 // contract function inputs).
309 tracker_by_address_handle: RefCell<BTreeMap<u32, usize>>,
310 // Whether to allow root authorized invocation to not match the root
311 // contract invocation.
312 disable_non_root_auth: bool,
313}
314
315#[derive(Clone, Hash)]
316enum AuthorizationMode {
317 Enforcing,
318 #[cfg(any(test, feature = "recording_mode"))]
319 Recording(RecordingAuthInfo),
320}
321
322#[cfg(any(test, feature = "recording_mode"))]
323impl std::hash::Hash for RecordingAuthInfo {
324 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
325 if let Ok(tracker_by_address_handle) = self.tracker_by_address_handle.try_borrow() {
326 tracker_by_address_handle.hash(state);
327 }
328 self.disable_non_root_auth.hash(state);
329 }
330}
331
332#[cfg(any(test, feature = "recording_mode"))]
333impl RecordingAuthInfo {
334 fn try_borrow_tracker_by_address_handle(
335 &self,
336 host: &Host,
337 ) -> Result<std::cell::Ref<'_, BTreeMap<u32, usize>>, HostError> {
338 self.tracker_by_address_handle.try_borrow_or_err_with(
339 host,
340 "recording_auth_info.tracker_by_address_handle.try_borrow failed",
341 )
342 }
343 fn try_borrow_tracker_by_address_handle_mut(
344 &self,
345 host: &Host,
346 ) -> Result<std::cell::RefMut<'_, BTreeMap<u32, usize>>, HostError> {
347 self.tracker_by_address_handle.try_borrow_mut_or_err_with(
348 host,
349 "recording_auth_info.tracker_by_address_handle.try_borrow_mut failed",
350 )
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
355enum MatchState {
356 Unmatched,
357 RootMatch,
358 SubMatch { index_in_parent: usize },
359}
360
361impl MatchState {
362 fn is_matched(&self) -> bool {
363 *self != MatchState::Unmatched
364 }
365}
366
367/// An `InvocationTracker` is responsible for incrementally matching a single
368/// [`AuthorizedInvocation`] tree against the actual invocation tree. In this
369/// way the nodes that make up the `AuthorizedInvocation` act as a pattern, and
370/// the `InvocationTracker` is an incremental pattern-matcher. The same
371/// `InvocationTracker` type is used as the pattern-matching sub-component of
372/// [`AccountAuthorizationTracker`] and [`InvokerContractAuthorizationTracker`],
373/// as the matching logic is the same in both cases.
374///
375/// The `InvocationTracker` maintains a [`InvocationTracker::match_stack`] of
376/// [`MatchState`] values that correspond to the frames in
377/// [`AuthorizationManager::call_stack`], pushing and popping those values as
378/// frames are are pushed and popped from that call stack. The values in the
379/// match stack are initially all [`MatchState::Unmatched`].
380///
381/// Matching is initiated by a contract calling
382/// [`AuthorizationManager::require_auth`], and the auth manager then works its
383/// way through many possible trackers asking each to try to match itself
384/// against the current [`AuthorizationManager::call_stack`] context, the last
385/// entry of which is the frame requesting the authorization.
386///
387/// A tracker may be active or inactive. If active, it means that the
388/// `InvocationTracker` has begun matching itself against the call stack
389/// already, and the frame requesting authorization will be matched against the
390/// _children_ of the last (deepest) already-matched node in the tracker's
391/// [`AuthorizedInvocation`]. If inactive, it means that the `InvocationTracker`
392/// has not yet begun matching and so the frame requesting authorization will be
393/// matched against the root node of the [`AuthorizedInvocation`]. Any
394/// successful match is recorded in the [`InvocationTracker::match_stack`] by
395/// overwriting the value at the match, changing it from
396/// [`MatchState::Unmatched`] to either [`MatchState::RootMatch`] or
397/// [`MatchState::SubMatch`] as appropriate. This match-extension logic is in
398/// [`InvocationTracker::maybe_extend_invocation_match`]
399///
400/// The active-ness of a tracker is defined by the
401/// [`AuthorizedInvocation::exhausted`] flag on the root node it's matching, as
402/// well as the continued presence of the [`MatchState::RootMatch`] node on the
403/// match stack. In other words: the tracker becomes "active" as soon as the
404/// root match is pushed on the match stack (which exhausts the root node), and
405/// the tracker stops being active when the root match is popped from the match
406/// stack. At this point the [`InvocationTracker::is_fully_processed`] flag is
407/// set.
408#[derive(Clone, Hash)]
409struct InvocationTracker {
410 // Root of the authorized invocation tree.
411 // The authorized invocation tree only contains the contract invocations
412 // that explicitly require authorization on behalf of the address.
413 root_authorized_invocation: AuthorizedInvocation,
414 // Stack that tracks the current match of the tree of authorized invocations
415 // against the actual invocations made by the host. There is one entry in
416 // this vector for each entry in [`AuthorizationManager::call_stack`]
417 // (unless the tracker has been temporary suppressed due to reentry).
418 //
419 // The values in the match stack are always initially
420 // `MatchState::Unmatched`. The match stack may (if the tracker is active)
421 // contain a subsequence of values in it beginning with
422 // `MatchState::RootMatch` and continuing with a mixture of
423 // `MatchState::SubMatch` and `MatchState::Unmatched` values, corresponding
424 // to frames in the call stack that match or are ignored (respectively) by
425 // nodes in the `AuthorizedInvocation` pattern tree. If this vector ever
426 // contains a subsequence starting with `MatchState::SubMatch` (i.e. without
427 // a root), or contains more than one `MatchState::RootMatch`, there is a
428 // logic error somewhere.
429 match_stack: Vec<MatchState>,
430 // If root invocation is exhausted, the index of the stack frame where it
431 // was exhausted (i.e. index in `match_stack`).
432 root_exhausted_frame: Option<usize>,
433 // Indicates whether this tracker is fully processed, i.e. the authorized
434 // root frame has been exhausted and then popped from the stack.
435 is_fully_processed: bool,
436}
437
438// Stores all the authorizations that are authorized by an address.
439// In the enforcing mode this performs authentication and makes sure that only
440// pre-authorized invocations can happen on behalf of the `address`.
441// In the recording mode this will record the invocations that are authorized
442// on behalf of the address.
443#[derive(Clone, Hash)]
444pub(crate) struct AccountAuthorizationTracker {
445 // Tracked address.
446 address: AddressObject,
447 // Helper for matching the tree that address authorized to the invocation
448 // tree.
449 invocation_tracker: InvocationTracker,
450 // Value representing the signature created by the address to authorize
451 // the invocations tracked here.
452 signature: Val,
453 // Indicates whether this is a tracker for the transaction source account.
454 is_transaction_source_account: bool,
455 // When `true`, indicates that the tracker has been successfully verified,
456 // specifically it has been authenticated and has nonce verified and
457 // consumed.
458 // When `false`, indicates that verification hasn't happened yet or
459 // that it hasn't been successful. The latter case is subtle - we don't cache
460 // the verification failures because a verification failure is not recoverable
461 // and thus is bound to be rolled back.
462 verified: bool,
463 // The value of nonce authorized by the address with its live_until ledger.
464 // Must not exist in the ledger.
465 nonce: Option<(i64, u32)>,
466}
467
468pub(crate) struct AccountAuthorizationTrackerSnapshot {
469 invocation_tracker_root_snapshot: AuthorizedInvocationSnapshot,
470 verified: bool,
471}
472
473// Stores all the authorizations performed by contracts at runtime.
474#[derive(Clone, Hash)]
475pub(crate) struct InvokerContractAuthorizationTracker {
476 contract_address: AddressObject,
477 invocation_tracker: InvocationTracker,
478}
479
480#[derive(Clone, Hash)]
481pub(crate) enum AuthStackFrame {
482 Contract(ContractInvocation),
483 CreateContractHostFn(CreateContractArgsV2),
484}
485
486#[derive(Clone, Hash)]
487pub(crate) struct ContractInvocation {
488 pub(crate) contract_address: AddressObject,
489 pub(crate) function_name: Symbol,
490}
491
492#[derive(Clone, Hash)]
493pub(crate) struct ContractFunction {
494 pub(crate) contract_address: AddressObject,
495 pub(crate) function_name: Symbol,
496 pub(crate) args: Vec<Val>,
497}
498
499#[derive(Clone, Hash)]
500pub(crate) enum AuthorizedFunction {
501 ContractFn(ContractFunction),
502 CreateContractHostFn(CreateContractArgsV2),
503}
504
505// A single node in the authorized invocation tree.
506// This represents an invocation and all it's authorized sub-invocations.
507#[derive(Clone, Hash)]
508pub(crate) struct AuthorizedInvocation {
509 pub(crate) function: AuthorizedFunction,
510 pub(crate) sub_invocations: Vec<AuthorizedInvocation>,
511 // Indicates that this invocation has been already used in the
512 // enforcing mode. Exhausted authorizations can't be reused.
513 // In the recording mode this is immediately set to `true` (as the
514 // authorizations are recorded when they actually happen).
515 is_exhausted: bool,
516}
517
518// Snapshot of `AuthorizedInvocation` that contains only mutable fields.
519pub(crate) struct AuthorizedInvocationSnapshot {
520 is_exhausted: bool,
521 sub_invocations: Vec<AuthorizedInvocationSnapshot>,
522}
523
524impl Compare<ContractFunction> for Host {
525 type Error = HostError;
526
527 // metering: covered by host
528 fn compare(
529 &self,
530 a: &ContractFunction,
531 b: &ContractFunction,
532 ) -> Result<std::cmp::Ordering, Self::Error> {
533 let ord = self.compare(&a.contract_address, &b.contract_address)?;
534 if !ord.is_eq() {
535 return Ok(ord);
536 }
537 let ord = self.compare(&a.function_name, &b.function_name)?;
538 if !ord.is_eq() {
539 return Ok(ord);
540 }
541 self.compare(&a.args, &b.args)
542 }
543}
544
545impl Compare<AuthorizedFunction> for Host {
546 type Error = HostError;
547
548 // metering: covered by components
549 fn compare(
550 &self,
551 a: &AuthorizedFunction,
552 b: &AuthorizedFunction,
553 ) -> Result<std::cmp::Ordering, Self::Error> {
554 match (a, b) {
555 (AuthorizedFunction::ContractFn(f1), AuthorizedFunction::ContractFn(f2)) => {
556 self.compare(f1, f2)
557 }
558 (
559 AuthorizedFunction::CreateContractHostFn(c1),
560 AuthorizedFunction::CreateContractHostFn(c2),
561 ) => self.compare(c1, c2),
562 (AuthorizedFunction::ContractFn(_), AuthorizedFunction::CreateContractHostFn(_)) => {
563 Ok(std::cmp::Ordering::Less)
564 }
565 (AuthorizedFunction::CreateContractHostFn(_), AuthorizedFunction::ContractFn(_)) => {
566 Ok(std::cmp::Ordering::Greater)
567 }
568 }
569 }
570}
571
572impl AuthStackFrame {
573 // metering: covered
574 fn to_authorized_function(
575 &self,
576 host: &Host,
577 args: Vec<Val>,
578 ) -> Result<AuthorizedFunction, HostError> {
579 match self {
580 AuthStackFrame::Contract(contract_frame) => {
581 Ok(AuthorizedFunction::ContractFn(ContractFunction {
582 contract_address: contract_frame.contract_address,
583 function_name: contract_frame.function_name.metered_clone(host)?,
584 args,
585 }))
586 }
587 AuthStackFrame::CreateContractHostFn(args) => Ok(
588 AuthorizedFunction::CreateContractHostFn(args.metered_clone(host)?),
589 ),
590 }
591 }
592}
593
594impl AuthorizedFunction {
595 // metering: covered by the host
596 fn from_xdr(host: &Host, xdr_fn: SorobanAuthorizedFunction) -> Result<Self, HostError> {
597 Ok(match xdr_fn {
598 SorobanAuthorizedFunction::ContractFn(xdr_contract_fn) => {
599 AuthorizedFunction::ContractFn(ContractFunction {
600 contract_address: host.add_host_object(xdr_contract_fn.contract_address)?,
601 function_name: Symbol::try_from_val(host, &xdr_contract_fn.function_name)?,
602 args: host.scvals_to_val_vec(xdr_contract_fn.args.as_slice())?,
603 })
604 }
605 SorobanAuthorizedFunction::CreateContractHostFn(xdr_args) => {
606 AuthorizedFunction::CreateContractHostFn(CreateContractArgsV2 {
607 contract_id_preimage: xdr_args.contract_id_preimage,
608 executable: xdr_args.executable,
609 constructor_args: Default::default(),
610 })
611 }
612 SorobanAuthorizedFunction::CreateContractV2HostFn(xdr_args) => {
613 AuthorizedFunction::CreateContractHostFn(xdr_args)
614 }
615 })
616 }
617
618 // metering: covered by the host
619 fn to_xdr(&self, host: &Host) -> Result<SorobanAuthorizedFunction, HostError> {
620 match self {
621 AuthorizedFunction::ContractFn(contract_fn) => {
622 let function_name = host.scsymbol_from_symbol(contract_fn.function_name)?;
623 Ok(SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
624 contract_address: host.scaddress_from_address(contract_fn.contract_address)?,
625 function_name,
626 args: host.vals_to_scval_vec(contract_fn.args.as_slice())?,
627 }))
628 }
629 AuthorizedFunction::CreateContractHostFn(create_contract_args) => {
630 Ok(SorobanAuthorizedFunction::CreateContractV2HostFn(
631 create_contract_args.metered_clone(host)?,
632 ))
633 }
634 }
635 }
636}
637
638impl AuthorizedInvocation {
639 // metering: covered
640 fn from_xdr(
641 host: &Host,
642 xdr_invocation: xdr::SorobanAuthorizedInvocation,
643 ) -> Result<Self, HostError> {
644 let sub_invocations_xdr = xdr_invocation.sub_invocations.into_vec();
645 let sub_invocations = sub_invocations_xdr
646 .into_iter()
647 .map(|a| AuthorizedInvocation::from_xdr(host, a))
648 .metered_collect::<Result<Vec<_>, _>>(host)??;
649 Ok(Self {
650 function: AuthorizedFunction::from_xdr(host, xdr_invocation.function)?,
651 sub_invocations,
652 is_exhausted: false,
653 })
654 }
655
656 // metering: free
657 pub(crate) fn new(
658 function: AuthorizedFunction,
659 sub_invocations: Vec<AuthorizedInvocation>,
660 ) -> Self {
661 Self {
662 function,
663 sub_invocations,
664 is_exhausted: false,
665 }
666 }
667
668 // metering: free
669 #[cfg(any(test, feature = "recording_mode"))]
670 fn new_recording(function: AuthorizedFunction) -> Self {
671 Self {
672 function,
673 sub_invocations: vec![],
674 is_exhausted: true,
675 }
676 }
677
678 // metering: covered
679 fn to_xdr(
680 &self,
681 host: &Host,
682 exhausted_sub_invocations_only: bool,
683 ) -> Result<xdr::SorobanAuthorizedInvocation, HostError> {
684 Ok(xdr::SorobanAuthorizedInvocation {
685 function: self.function.to_xdr(host)?,
686 sub_invocations: self
687 .sub_invocations
688 .iter()
689 .filter(|i| i.is_exhausted || !exhausted_sub_invocations_only)
690 .map(|i| i.to_xdr(host, exhausted_sub_invocations_only))
691 .metered_collect::<Result<Vec<xdr::SorobanAuthorizedInvocation>, HostError>>(host)??
692 .try_into()?,
693 })
694 }
695
696 // Walks a path in the tree defined by `match_stack` and
697 // returns the last visited authorized node.
698 // metering: free
699 fn last_authorized_invocation_mut(
700 &mut self,
701 match_stack: &Vec<MatchState>,
702 call_stack_id: usize,
703 ) -> Result<&mut AuthorizedInvocation, HostError> {
704 // Start walking the stack from `call_stack_id`. We trust the callers to
705 // hold the invariant that `match_stack[call_stack_id - 1]`
706 // corresponds to this invocation tree, so that the next non-`None` child
707 // corresponds to the child of the current tree.
708 for (i, m) in match_stack.iter().enumerate().skip(call_stack_id) {
709 match m {
710 MatchState::SubMatch { index_in_parent } => {
711 // We trust the caller to have the correct sub-invocation
712 // indices.
713 if let Some(sub) = self.sub_invocations.get_mut(*index_in_parent) {
714 return sub.last_authorized_invocation_mut(match_stack, i + 1);
715 } else {
716 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
717 }
718 }
719 MatchState::RootMatch => {
720 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
721 }
722 // Skip Unmatched invocations as they don't require authorization.
723 MatchState::Unmatched => (),
724 }
725 }
726 Ok(self)
727 }
728
729 // metering: covered
730 fn snapshot(&self, budget: &Budget) -> Result<AuthorizedInvocationSnapshot, HostError> {
731 Ok(AuthorizedInvocationSnapshot {
732 is_exhausted: self.is_exhausted,
733 sub_invocations: self
734 .sub_invocations
735 .iter()
736 .map(|i| i.snapshot(budget))
737 .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(
738 budget,
739 )??,
740 })
741 }
742
743 // metering: free
744 fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> {
745 self.is_exhausted = snapshot.is_exhausted;
746 if self.sub_invocations.len() != snapshot.sub_invocations.len() {
747 // This would be a bug.
748 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
749 }
750 for (sub, snap) in self
751 .sub_invocations
752 .iter_mut()
753 .zip(snapshot.sub_invocations.iter())
754 {
755 sub.rollback(snap)?
756 }
757 Ok(())
758 }
759}
760
761impl Default for AuthorizationManager {
762 fn default() -> Self {
763 Self::new_enforcing_without_authorizations()
764 }
765}
766
767impl AuthorizationManager {
768 // Creates a new enforcing `AuthorizationManager` from the given
769 // authorization entries.
770 // This should be created once per top-level invocation.
771 // metering: covered
772 pub(crate) fn new_enforcing(
773 host: &Host,
774 auth_entries: Vec<SorobanAuthorizationEntry>,
775 ) -> Result<Self, HostError> {
776 let mut trackers = Vec::<RefCell<AccountAuthorizationTracker>>::with_metered_capacity(
777 auth_entries.len(),
778 host,
779 )?;
780 for auth_entry in auth_entries {
781 trackers.push(RefCell::new(
782 AccountAuthorizationTracker::from_authorization_entry(host, auth_entry)?,
783 ));
784 }
785 Ok(Self {
786 mode: AuthorizationMode::Enforcing,
787 call_stack: RefCell::new(vec![]),
788 account_trackers: RefCell::new(trackers),
789 invoker_contract_trackers: RefCell::new(vec![]),
790 })
791 }
792
793 // Creates a new enforcing `AuthorizationManager` that doesn't allow any
794 // authorizations.
795 // This is useful as a safe default mode.
796 // metering: free
797 pub(crate) fn new_enforcing_without_authorizations() -> Self {
798 Self {
799 mode: AuthorizationMode::Enforcing,
800 call_stack: RefCell::new(vec![]),
801 account_trackers: RefCell::new(vec![]),
802 invoker_contract_trackers: RefCell::new(vec![]),
803 }
804 }
805
806 // Creates a new recording `AuthorizationManager`.
807 // All the authorization requirements will be recorded and can then be
808 // retrieved using `get_recorded_auth_payloads`.
809 // metering: free
810 #[cfg(any(test, feature = "recording_mode"))]
811 pub(crate) fn new_recording(disable_non_root_auth: bool) -> Self {
812 Self {
813 mode: AuthorizationMode::Recording(RecordingAuthInfo {
814 tracker_by_address_handle: Default::default(),
815 disable_non_root_auth,
816 }),
817 call_stack: RefCell::new(vec![]),
818 account_trackers: RefCell::new(vec![]),
819 invoker_contract_trackers: RefCell::new(vec![]),
820 }
821 }
822
823 // Require the `address` to have authorized the current contract invocation
824 // with provided args and within the current context (i.e. the current
825 // authorized call stack and for the current network).
826 // In the recording mode this stores the auth requirement instead of
827 // verifying it.
828 // metering: covered
829 pub(crate) fn require_auth(
830 &self,
831 host: &Host,
832 address: AddressObject,
833 args: Vec<Val>,
834 ) -> Result<(), HostError> {
835 let _span = tracy_span!("require auth");
836 let authorized_function = self
837 .try_borrow_call_stack(host)?
838 .last()
839 .ok_or_else(|| {
840 host.err(
841 ScErrorType::Auth,
842 ScErrorCode::InternalError,
843 "unexpected require_auth outside of valid frame",
844 &[],
845 )
846 })?
847 .to_authorized_function(host, args)?;
848
849 self.require_auth_internal(host, address, authorized_function)
850 }
851
852 // metering: covered
853 pub(crate) fn add_invoker_contract_auth_with_curr_contract_as_invoker(
854 &self,
855 host: &Host,
856 auth_entries: VecObject,
857 ) -> Result<(), HostError> {
858 let auth_entries =
859 host.visit_obj(auth_entries, |e: &HostVec| e.to_vec(host.budget_ref()))?;
860 let mut trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
861 Vec::<InvokerContractAuthorizationTracker>::charge_bulk_init_cpy(
862 auth_entries.len() as u64,
863 host,
864 )?;
865 trackers.reserve(auth_entries.len());
866 for e in auth_entries {
867 trackers.push(
868 InvokerContractAuthorizationTracker::new_with_curr_contract_as_invoker(host, e)?,
869 )
870 }
871 Ok(())
872 }
873
874 // metering: covered by components
875 fn maybe_check_invoker_contract_auth(
876 &self,
877 host: &Host,
878 address: AddressObject,
879 function: &AuthorizedFunction,
880 ) -> Result<bool, HostError> {
881 {
882 let call_stack = self.try_borrow_call_stack(host)?;
883 // If stack has just one call there can't be invoker.
884 if call_stack.len() < 2 {
885 return Ok(false);
886 }
887
888 // Try matching the direct invoker contract first. It is considered to
889 // have authorized any direct calls.
890 let Some(invoker_frame) = &call_stack.get(call_stack.len() - 2) else {
891 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
892 };
893 if let AuthStackFrame::Contract(invoker_contract) = invoker_frame {
894 if host
895 .compare(&invoker_contract.contract_address, &address)?
896 .is_eq()
897 {
898 return Ok(true);
899 }
900 }
901 }
902 let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
903 // If there is no direct invoker, there still might be a valid
904 // sub-contract call authorization from another invoker higher up the
905 // stack. Note, that invoker contract trackers consider the direct frame
906 // to never require auth (any `require_auth` calls would be matched by
907 // logic above).
908 for tracker in invoker_contract_trackers.iter_mut() {
909 if host.compare(&tracker.contract_address, &address)?.is_eq()
910 && tracker.maybe_authorize_invocation(host, function)?
911 {
912 return Ok(true);
913 }
914 }
915
916 return Ok(false);
917 }
918
919 // metering: covered by components
920 fn require_auth_enforcing(
921 &self,
922 host: &Host,
923 address: AddressObject,
924 function: &AuthorizedFunction,
925 ) -> Result<(), HostError> {
926 // Find if there is already an active tracker for this address that has
927 // not been matched for the current frame. If there is such tracker,
928 // this authorization has to be matched with an already active tracker.
929 // This prevents matching sets of disjoint authorization entries to
930 // a tree of calls.
931 let mut has_active_tracker = false;
932 for tracker in self.try_borrow_account_trackers(host)?.iter() {
933 if let Ok(tracker) = tracker.try_borrow() {
934 // If address doesn't match, just skip the tracker.
935 if host.compare(&tracker.address, &address)?.is_eq()
936 && tracker.is_active()
937 && !tracker.current_frame_is_already_matched()
938 {
939 has_active_tracker = true;
940 break;
941 }
942 }
943 }
944
945 // Iterate all the trackers and try to find one that
946 // fulfills the authorization requirement.
947 for tracker in self.try_borrow_account_trackers(host)?.iter() {
948 // Tracker can only be borrowed by the authorization manager itself.
949 // The only scenario in which re-borrow might occur is when
950 // `require_auth` is called within `__check_auth` call. The tracker
951 // that called `__check_auth` would be already borrowed in such
952 // scenario.
953 // We allow such call patterns in general, but we don't allow using
954 // tracker to verify auth for itself, i.e. we don't allow something
955 // like address.require_auth()->address_contract.__check_auth()
956 // ->address.require_auth(). Thus we simply skip the trackers that
957 // have already been borrowed.
958 if let Ok(mut tracker) = tracker.try_borrow_mut() {
959 // If tracker has already been used for this frame or the address
960 // doesn't match, just skip the tracker.
961 if !host.compare(&tracker.address, &address)?.is_eq() {
962 continue;
963 }
964 match tracker.maybe_authorize_invocation(host, function, !has_active_tracker) {
965 // If tracker doesn't have a matching invocation,
966 // just skip it (there could still be another
967 // tracker that matches it).
968 Ok(false) => continue,
969 // Found a matching authorization.
970 Ok(true) => return Ok(()),
971 // Found a matching authorization, but another
972 // requirement hasn't been fulfilled (for
973 // example, incorrect authentication or nonce).
974 Err(e) => return Err(e),
975 }
976 }
977 }
978 // No matching tracker found, hence the invocation isn't
979 // authorized.
980 Err(host.err(
981 ScErrorType::Auth,
982 ScErrorCode::InvalidAction,
983 "Unauthorized function call for address",
984 &[address.to_val()],
985 ))
986 }
987
988 #[cfg(any(test, feature = "recording_mode"))]
989 fn require_auth_recording(
990 &self,
991 host: &Host,
992 address: AddressObject,
993 function: AuthorizedFunction,
994 recording_info: &RecordingAuthInfo,
995 ) -> Result<(), HostError> {
996 // At first, try to find the tracker for this exact address
997 // object.
998 // This is a best-effort heuristic to come up with a reasonably
999 // looking recording tree for cases when multiple instances of
1000 // the same exact address are used.
1001 let address_obj_handle = address.get_handle();
1002 let existing_tracker_id = recording_info
1003 .try_borrow_tracker_by_address_handle(host)?
1004 .get(&address_obj_handle)
1005 .copied();
1006 if let Some(tracker_id) = existing_tracker_id {
1007 // The tracker should not be borrowed recursively in
1008 // recording mode, as we don't call `__check_auth` in this
1009 // flow.
1010 let trackers = self.try_borrow_account_trackers(host)?;
1011 let Some(trackercell) = trackers.get(tracker_id) else {
1012 return Err(host.err(
1013 ScErrorType::Auth,
1014 ScErrorCode::InternalError,
1015 "bad index for existing tracker",
1016 &[],
1017 ));
1018 };
1019 if let Ok(mut tracker) = trackercell.try_borrow_mut() {
1020 // The recording invariant is that trackers are created
1021 // with the first authorized invocation, which means
1022 // that when their stack no longer has authorized
1023 // invocation, then we've popped frames past its root
1024 // and hence need to create a new tracker.
1025 if !tracker.has_authorized_invocations_in_stack() {
1026 recording_info
1027 .try_borrow_tracker_by_address_handle_mut(host)?
1028 .remove(&address_obj_handle);
1029 } else {
1030 return tracker.record_invocation(host, function);
1031 }
1032 } else {
1033 return Err(host.err(
1034 ScErrorType::Auth,
1035 ScErrorCode::InternalError,
1036 "unexpected recursive tracker borrow in recording mode",
1037 &[],
1038 ));
1039 };
1040 }
1041 // If there is no active tracker for this exact address object,
1042 // try to find any matching active tracker for the address.
1043 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1044 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1045 if !host.compare(&tracker.address, &address)?.is_eq() {
1046 continue;
1047 }
1048 // Take the first tracker that is still active (i.e. has
1049 // active authorizations in the current call stack) and
1050 // hasn't been used for this stack frame yet.
1051 if tracker.has_authorized_invocations_in_stack()
1052 && !tracker.current_frame_is_already_matched()
1053 {
1054 return tracker.record_invocation(host, function);
1055 }
1056 } else {
1057 return Err(host.err(
1058 ScErrorType::Auth,
1059 ScErrorCode::InternalError,
1060 "unexpected borrowed tracker in recording auth mode",
1061 &[],
1062 ));
1063 }
1064 }
1065 // At this stage there is no active tracker to which we could
1066 // match the current invocation, thus we need to create a new
1067 // tracker.
1068 // Alert the user in `disable_non_root_auth` mode if we're not
1069 // in the root stack frame.
1070 if recording_info.disable_non_root_auth && self.try_borrow_call_stack(host)?.len() != 1 {
1071 return Err(host.err(
1072 ScErrorType::Auth,
1073 ScErrorCode::InvalidAction,
1074 "[recording authorization only] encountered authorization not tied \
1075 to the root contract invocation for an address. Use `require_auth()` \
1076 in the top invocation or enable non-root authorization.",
1077 &[address.into()],
1078 ));
1079 }
1080 // If a tracker for the new tree doesn't exist yet, create
1081 // it and initialize with the current invocation.
1082 self.try_borrow_account_trackers_mut(host)?
1083 .push(RefCell::new(AccountAuthorizationTracker::new_recording(
1084 host,
1085 address,
1086 function,
1087 self.try_borrow_call_stack(host)?.len(),
1088 )?));
1089 recording_info
1090 .try_borrow_tracker_by_address_handle_mut(host)?
1091 .insert(
1092 address_obj_handle,
1093 self.try_borrow_account_trackers(host)?.len() - 1,
1094 );
1095 Ok(())
1096 }
1097
1098 // metering: covered
1099 fn require_auth_internal(
1100 &self,
1101 host: &Host,
1102 address: AddressObject,
1103 function: AuthorizedFunction,
1104 ) -> Result<(), HostError> {
1105 // First check the InvokerContractAuthorizationTrackers
1106 if self.maybe_check_invoker_contract_auth(host, address, &function)? {
1107 return Ok(());
1108 }
1109 // Then check the AccountAuthorizationTrackers
1110 match &self.mode {
1111 AuthorizationMode::Enforcing => self.require_auth_enforcing(host, address, &function),
1112 // metering: free for recording
1113 #[cfg(any(test, feature = "recording_mode"))]
1114 AuthorizationMode::Recording(recording_info) => {
1115 self.require_auth_recording(host, address, function, recording_info)
1116 }
1117 }
1118 }
1119
1120 // Returns a snapshot of `AuthorizationManager` to use for rollback.
1121 // metering: covered
1122 fn snapshot(&self, host: &Host) -> Result<AuthorizationManagerSnapshot, HostError> {
1123 let _span = tracy_span!("snapshot auth");
1124 let account_trackers_snapshot = match &self.mode {
1125 AuthorizationMode::Enforcing => {
1126 let len = self.try_borrow_account_trackers(host)?.len();
1127 let mut snapshots =
1128 Vec::<Option<AccountAuthorizationTrackerSnapshot>>::with_metered_capacity(
1129 len, host,
1130 )?;
1131 for t in self.try_borrow_account_trackers(host)?.iter() {
1132 let sp = if let Ok(tracker) = t.try_borrow() {
1133 Some(tracker.snapshot(host.as_budget())?)
1134 } else {
1135 // If tracker is borrowed, snapshotting it is a no-op
1136 // (it can't change until we release it higher up the
1137 // stack).
1138 None
1139 };
1140 snapshots.push(sp);
1141 }
1142 AccountTrackersSnapshot::Enforcing(snapshots)
1143 }
1144 #[cfg(any(test, feature = "recording_mode"))]
1145 AuthorizationMode::Recording(_) => {
1146 // All trackers should be available to borrow for copy as in
1147 // recording mode we can't have recursive authorization.
1148 // metering: free for recording
1149 AccountTrackersSnapshot::Recording(self.try_borrow_account_trackers(host)?.clone())
1150 }
1151 };
1152 let invoker_contract_tracker_root_snapshots = self
1153 .try_borrow_invoker_contract_trackers(host)?
1154 .iter()
1155 .map(|t| t.invocation_tracker.snapshot(host.as_budget()))
1156 .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(host)??;
1157 #[cfg(any(test, feature = "recording_mode"))]
1158 let tracker_by_address_handle = match &self.mode {
1159 AuthorizationMode::Enforcing => None,
1160 AuthorizationMode::Recording(recording_info) => Some(
1161 // metering: free for recording
1162 recording_info
1163 .try_borrow_tracker_by_address_handle(host)?
1164 .clone(),
1165 ),
1166 };
1167 Ok(AuthorizationManagerSnapshot {
1168 account_trackers_snapshot,
1169 invoker_contract_tracker_root_snapshots,
1170 #[cfg(any(test, feature = "recording_mode"))]
1171 tracker_by_address_handle,
1172 })
1173 }
1174
1175 // Rolls back this `AuthorizationManager` to the snapshot state.
1176 // metering: covered
1177 fn rollback(
1178 &self,
1179 host: &Host,
1180 snapshot: AuthorizationManagerSnapshot,
1181 ) -> Result<(), HostError> {
1182 let _span = tracy_span!("rollback auth");
1183 match snapshot.account_trackers_snapshot {
1184 AccountTrackersSnapshot::Enforcing(trackers_snapshot) => {
1185 let trackers = self.try_borrow_account_trackers(host)?;
1186 if trackers.len() != trackers_snapshot.len() {
1187 return Err(host.err(
1188 ScErrorType::Auth,
1189 ScErrorCode::InternalError,
1190 "unexpected bad auth snapshot",
1191 &[],
1192 ));
1193 }
1194 for (i, tracker) in trackers.iter().enumerate() {
1195 let Some(snapopt) = trackers_snapshot.get(i) else {
1196 return Err(host.err(
1197 ScErrorType::Auth,
1198 ScErrorCode::InternalError,
1199 "unexpected auth snapshot index",
1200 &[],
1201 ));
1202 };
1203 if let Some(tracker_snapshot) = snapopt {
1204 tracker
1205 .try_borrow_mut()
1206 .map_err(|_| {
1207 host.err(
1208 ScErrorType::Auth,
1209 ScErrorCode::InternalError,
1210 "unexpected bad auth borrow",
1211 &[],
1212 )
1213 })?
1214 .rollback(&tracker_snapshot)?;
1215 }
1216 }
1217 }
1218 #[cfg(any(test, feature = "recording_mode"))]
1219 AccountTrackersSnapshot::Recording(s) => {
1220 *self.try_borrow_account_trackers_mut(host)? = s;
1221 }
1222 }
1223
1224 let mut invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1225 if invoker_trackers.len() != snapshot.invoker_contract_tracker_root_snapshots.len() {
1226 return Err(host.err(
1227 ScErrorType::Auth,
1228 ScErrorCode::InternalError,
1229 "unexpected bad auth snapshot",
1230 &[],
1231 ));
1232 }
1233 for (tracker, snapshot) in invoker_trackers
1234 .iter_mut()
1235 .zip(snapshot.invoker_contract_tracker_root_snapshots.iter())
1236 {
1237 tracker.invocation_tracker.rollback(snapshot)?;
1238 }
1239
1240 #[cfg(any(test, feature = "recording_mode"))]
1241 if let Some(tracker_by_address_handle) = snapshot.tracker_by_address_handle {
1242 match &self.mode {
1243 AuthorizationMode::Enforcing => (),
1244 AuthorizationMode::Recording(recording_info) => {
1245 *recording_info.try_borrow_tracker_by_address_handle_mut(host)? =
1246 tracker_by_address_handle;
1247 }
1248 }
1249 }
1250 Ok(())
1251 }
1252
1253 // metering: covered
1254 fn push_tracker_frame(&self, host: &Host) -> Result<(), HostError> {
1255 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1256 // Skip already borrowed trackers, these must be in the middle of
1257 // authentication and hence don't need stack to be updated.
1258 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1259 tracker.push_frame(host.as_budget())?;
1260 }
1261 }
1262 for tracker in self
1263 .try_borrow_invoker_contract_trackers_mut(host)?
1264 .iter_mut()
1265 {
1266 tracker.push_frame(host.as_budget())?;
1267 }
1268 Ok(())
1269 }
1270
1271 // metering: covered
1272 pub(crate) fn push_create_contract_host_fn_frame(
1273 &self,
1274 host: &Host,
1275 args: CreateContractArgsV2,
1276 ) -> Result<(), HostError> {
1277 Vec::<CreateContractArgsV2>::charge_bulk_init_cpy(1, host)?;
1278 self.try_borrow_call_stack_mut(host)?
1279 .push(AuthStackFrame::CreateContractHostFn(args));
1280 self.push_tracker_frame(host)
1281 }
1282
1283 // Records a new call stack frame and returns a snapshot for rolling
1284 // back this stack frame.
1285 // This should be called for every `Host` `push_frame`.
1286 // metering: covered
1287 pub(crate) fn push_frame(
1288 &self,
1289 host: &Host,
1290 frame: &Frame,
1291 ) -> Result<AuthorizationManagerSnapshot, HostError> {
1292 let _span = tracy_span!("push auth frame");
1293 let (contract_id, function_name) = match frame {
1294 Frame::ContractVM { vm, fn_name, .. } => {
1295 (vm.contract_id.metered_clone(host)?, *fn_name)
1296 }
1297 // Skip the top-level host function stack frames as they don't
1298 // contain all the necessary information.
1299 // Use the respective push (like
1300 // `push_create_contract_host_fn_frame`) functions instead to push
1301 // the frame with the required info.
1302 Frame::HostFunction(_) => return self.snapshot(host),
1303 Frame::StellarAssetContract(id, fn_name, ..) => (id.metered_clone(host)?, *fn_name),
1304 #[cfg(any(test, feature = "testutils"))]
1305 Frame::TestContract(tc) => (tc.id.metered_clone(host)?, tc.func),
1306 };
1307 let contract_address = host.add_host_object(ScAddress::Contract(contract_id))?;
1308 Vec::<ContractInvocation>::charge_bulk_init_cpy(1, host)?;
1309 self.try_borrow_call_stack_mut(host)?
1310 .push(AuthStackFrame::Contract(ContractInvocation {
1311 contract_address,
1312 function_name,
1313 }));
1314
1315 self.push_tracker_frame(host)?;
1316 self.snapshot(host)
1317 }
1318
1319 // Pops a call stack frame and maybe rolls back the internal
1320 // state according to the provided snapshot.
1321 // This should be called for every `Host` `pop_frame`.
1322 // metering: covered
1323 pub(crate) fn pop_frame(
1324 &self,
1325 host: &Host,
1326 snapshot: Option<AuthorizationManagerSnapshot>,
1327 ) -> Result<(), HostError> {
1328 let _span = tracy_span!("pop auth frame");
1329 // Important: rollback has to be performed before popping the frame
1330 // from the tracker. This ensures correct work of invoker contract
1331 // trackers for which snapshots are dependent on the current
1332 // call stack.
1333 if let Some(snapshot) = snapshot {
1334 self.rollback(host, snapshot)?;
1335 }
1336 {
1337 let mut call_stack = self.try_borrow_call_stack_mut(host)?;
1338 // Currently we don't push host function call frames, hence this may be
1339 // called with empty stack. We trust the Host to keep things correct,
1340 // i.e. that only host function frames are ignored this way.
1341 if call_stack.is_empty() {
1342 return Ok(());
1343 }
1344 call_stack.pop();
1345 }
1346 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1347 // Skip already borrowed trackers, these must be in the middle of
1348 // authentication and hence don't need stack to be updated.
1349 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1350 tracker.pop_frame();
1351 }
1352 }
1353
1354 let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1355 for tracker in invoker_contract_trackers.iter_mut() {
1356 tracker.pop_frame();
1357 }
1358 // Pop invoker contract trackers that went out of scope. The invariant
1359 // is that tracker only exists for the next sub-contract call (or until
1360 // the tracker's frame itself is popped). Thus trackers form a stack
1361 // where the shorter lifetime trackers are at the top.
1362 while let Some(last) = invoker_contract_trackers.last() {
1363 // *Subtle*: there are two possible scenarios when tracker is considered
1364 // to be out of scope:
1365 // - When the sub-contract call is finished. For example, contract A creates
1366 // a tracker, then calls contract B. When we pop the frame of contract B,
1367 // the tracker's stack will be empty and thus considered to be out of scope.
1368 // - When the contract that created the tracker is going out of scope without
1369 // calling any sub-contracts. For example, contract A creates a tracker and
1370 // returns. When we pop the frame of contract A, the tracker's stack will be
1371 // empty (because it's created empty), and thus considered to be out of scope.
1372 // The invariant above is maintained in both scenarios.
1373 if last.is_out_of_scope() {
1374 invoker_contract_trackers.pop();
1375 } else {
1376 break;
1377 }
1378 }
1379 Ok(())
1380 }
1381
1382 // Returns the recorded per-address authorization payloads that would cover the
1383 // top-level contract function invocation in the enforcing mode.
1384 // Should only be called in the recording mode.
1385 // metering: free, recording mode
1386 #[cfg(any(test, feature = "recording_mode"))]
1387 pub(crate) fn get_recorded_auth_payloads(
1388 &self,
1389 host: &Host,
1390 ) -> Result<Vec<RecordedAuthPayload>, HostError> {
1391 match &self.mode {
1392 AuthorizationMode::Enforcing => Err(HostError::from((
1393 ScErrorType::Auth,
1394 ScErrorCode::InternalError,
1395 ))),
1396 AuthorizationMode::Recording(_) => Ok(self
1397 .try_borrow_account_trackers(host)?
1398 .iter()
1399 .map(|tracker| tracker.try_borrow_or_err()?.get_recorded_auth_payload(host))
1400 .collect::<Result<Vec<RecordedAuthPayload>, HostError>>()?),
1401 }
1402 }
1403
1404 // For recording mode, emulates authentication that would normally happen in
1405 // the enforcing mode.
1406 // This helps to build a more realistic footprint and produce more correct
1407 // meterting data for the recording mode.
1408 // No-op in the enforcing mode.
1409 // metering: covered
1410 #[cfg(any(test, feature = "recording_mode"))]
1411 pub(crate) fn maybe_emulate_authentication(&self, host: &Host) -> Result<(), HostError> {
1412 match &self.mode {
1413 AuthorizationMode::Enforcing => Ok(()),
1414 AuthorizationMode::Recording(_) => {
1415 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1416 tracker
1417 .try_borrow_mut_or_err()?
1418 .emulate_authentication(host)?;
1419 }
1420 Ok(())
1421 }
1422 }
1423 }
1424
1425 // Returns a 'reset' instance of `AuthorizationManager` that has the same
1426 // mode, but no data.
1427 // metering: free, testutils
1428 #[cfg(any(test, feature = "testutils"))]
1429 pub(crate) fn reset(&mut self) {
1430 *self = match &self.mode {
1431 AuthorizationMode::Enforcing => {
1432 AuthorizationManager::new_enforcing_without_authorizations()
1433 }
1434 AuthorizationMode::Recording(rec_info) => {
1435 AuthorizationManager::new_recording(rec_info.disable_non_root_auth)
1436 }
1437 }
1438 }
1439
1440 // Returns all authorizations that have been authenticated for the
1441 // last contract invocation.
1442 // metering: free, testutils
1443 #[cfg(any(test, feature = "testutils"))]
1444 pub(crate) fn get_authenticated_authorizations(
1445 &self,
1446 host: &Host,
1447 ) -> Vec<(ScAddress, xdr::SorobanAuthorizedInvocation)> {
1448 host.as_budget()
1449 .with_observable_shadow_mode(|| {
1450 self.account_trackers
1451 .borrow()
1452 .iter()
1453 .filter(|t| t.borrow().verified)
1454 .map(|t| {
1455 (
1456 host.scaddress_from_address(t.borrow().address).unwrap(),
1457 t.borrow()
1458 .invocation_tracker
1459 .root_authorized_invocation
1460 .to_xdr(host, true)
1461 .unwrap(),
1462 )
1463 })
1464 .metered_collect(host)
1465 })
1466 .unwrap()
1467 }
1468}
1469
1470// Some helper extensions to support test-observation.
1471#[allow(dead_code)]
1472impl AuthorizationManager {
1473 pub(crate) fn stack_size(&self) -> usize {
1474 if let Ok(call_stack) = self.call_stack.try_borrow() {
1475 call_stack.len()
1476 } else {
1477 0
1478 }
1479 }
1480
1481 pub(crate) fn stack_hash(&self, budget: &Budget) -> Result<u64, HostError> {
1482 use std::hash::Hasher;
1483 if let Ok(call_stack) = self.call_stack.try_borrow() {
1484 let mut state = CountingHasher::default();
1485 call_stack.metered_hash(&mut state, budget)?;
1486 Ok(state.finish())
1487 } else {
1488 Ok(0)
1489 }
1490 }
1491
1492 pub(crate) fn trackers_hash_and_size(
1493 &self,
1494 budget: &Budget,
1495 ) -> Result<(u64, usize), HostError> {
1496 use std::hash::Hasher;
1497 let mut size: usize = 0;
1498 let mut state = CountingHasher::default();
1499 self.mode.metered_hash(&mut state, budget)?;
1500 if let Ok(account_trackers) = self.account_trackers.try_borrow() {
1501 for tracker in account_trackers.iter() {
1502 if let Ok(tracker) = tracker.try_borrow() {
1503 size = size.saturating_add(1);
1504 tracker.metered_hash(&mut state, budget)?;
1505 }
1506 }
1507 }
1508 if let Ok(invoker_contract_trackers) = self.invoker_contract_trackers.try_borrow() {
1509 for tracker in invoker_contract_trackers.iter() {
1510 size = size.saturating_add(1);
1511 tracker.metered_hash(&mut state, budget)?;
1512 }
1513 }
1514 Ok((state.finish(), size))
1515 }
1516}
1517
1518impl InvocationTracker {
1519 // metering: covered by components
1520 fn from_xdr(
1521 host: &Host,
1522 root_invocation: xdr::SorobanAuthorizedInvocation,
1523 ) -> Result<Self, HostError> {
1524 Ok(Self {
1525 root_authorized_invocation: AuthorizedInvocation::from_xdr(host, root_invocation)?,
1526 match_stack: vec![],
1527 root_exhausted_frame: None,
1528 is_fully_processed: false,
1529 })
1530 }
1531
1532 // metering: free
1533 fn new(root_authorized_invocation: AuthorizedInvocation) -> Self {
1534 Self {
1535 root_authorized_invocation,
1536 match_stack: vec![],
1537 root_exhausted_frame: None,
1538 is_fully_processed: false,
1539 }
1540 }
1541
1542 // metering: free for recording
1543 #[cfg(any(test, feature = "recording_mode"))]
1544 fn new_recording(function: AuthorizedFunction, current_stack_len: usize) -> Self {
1545 // Create the stack of `MatchState::Unmatched` leading to the current invocation to
1546 // represent invocations that didn't need authorization on behalf of
1547 // the tracked address.
1548 let mut match_stack = vec![MatchState::Unmatched; current_stack_len - 1];
1549 // Add a MatchState for the current(root) invocation.
1550 match_stack.push(MatchState::RootMatch);
1551 let root_exhausted_frame = Some(match_stack.len() - 1);
1552 Self {
1553 root_authorized_invocation: AuthorizedInvocation::new_recording(function),
1554 match_stack,
1555 root_exhausted_frame,
1556 is_fully_processed: false,
1557 }
1558 }
1559
1560 // Walks a path in the tree defined by `match_stack` and
1561 // returns the last visited authorized node.
1562 // metering: free
1563 fn last_authorized_invocation_mut(
1564 &mut self,
1565 ) -> Result<Option<&mut AuthorizedInvocation>, HostError> {
1566 for (i, m) in self.match_stack.iter().enumerate() {
1567 match m {
1568 MatchState::RootMatch => {
1569 return Ok(Some(
1570 self.root_authorized_invocation
1571 .last_authorized_invocation_mut(&self.match_stack, i + 1)?,
1572 ));
1573 }
1574 MatchState::SubMatch { .. } => {
1575 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into())
1576 }
1577 MatchState::Unmatched => (),
1578 }
1579 }
1580 Ok(None)
1581 }
1582
1583 // metering: covered
1584 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1585 Vec::<usize>::charge_bulk_init_cpy(1, budget)?;
1586 self.match_stack.push(MatchState::Unmatched);
1587 Ok(())
1588 }
1589
1590 // metering: free
1591 fn pop_frame(&mut self) {
1592 self.match_stack.pop();
1593 if let Some(root_exhausted_frame) = self.root_exhausted_frame {
1594 if root_exhausted_frame >= self.match_stack.len() {
1595 self.is_fully_processed = true;
1596 }
1597 }
1598 }
1599
1600 // metering: free
1601 fn is_empty(&self) -> bool {
1602 self.match_stack.is_empty()
1603 }
1604
1605 // metering: free
1606 fn is_active(&self) -> bool {
1607 self.root_authorized_invocation.is_exhausted && !self.is_fully_processed
1608 }
1609
1610 // metering: free
1611 fn current_frame_is_already_matched(&self) -> bool {
1612 match self.match_stack.last() {
1613 Some(x) => x.is_matched(),
1614 _ => false,
1615 }
1616 }
1617
1618 // Tries to match the provided invocation as an extension of the last
1619 // currently-matched sub-invocation of the authorized invocation tree (or
1620 // the root, if there is no matched invocation yet). If matching succeeds,
1621 // it writes the match to the corresponding entry in
1622 // [`InvocationTracker::match_stack`].
1623 //
1624 // Returns `true` if the match has been found for the first time per current
1625 // frame.
1626 //
1627 // Metering: covered by components
1628 fn maybe_extend_invocation_match(
1629 &mut self,
1630 host: &Host,
1631 function: &AuthorizedFunction,
1632 allow_matching_root: bool,
1633 ) -> Result<bool, HostError> {
1634 if self.current_frame_is_already_matched() {
1635 return Ok(false);
1636 }
1637 let mut new_match_state = MatchState::Unmatched;
1638 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1639 for (index_in_parent, sub_invocation) in
1640 curr_invocation.sub_invocations.iter_mut().enumerate()
1641 {
1642 if !sub_invocation.is_exhausted
1643 && host.compare(&sub_invocation.function, function)?.is_eq()
1644 {
1645 new_match_state = MatchState::SubMatch { index_in_parent };
1646 sub_invocation.is_exhausted = true;
1647 break;
1648 }
1649 }
1650 } else if !self.root_authorized_invocation.is_exhausted
1651 && allow_matching_root
1652 && host
1653 .compare(&self.root_authorized_invocation.function, &function)?
1654 .is_eq()
1655 {
1656 new_match_state = MatchState::RootMatch;
1657 self.root_authorized_invocation.is_exhausted = true;
1658 self.root_exhausted_frame = Some(self.match_stack.len() - 1);
1659 }
1660 if new_match_state.is_matched() {
1661 *self.match_stack.last_mut().ok_or_else(|| {
1662 host.err(
1663 ScErrorType::Auth,
1664 ScErrorCode::InternalError,
1665 "invalid match_stack",
1666 &[],
1667 )
1668 })? = new_match_state;
1669 }
1670 Ok(new_match_state.is_matched())
1671 }
1672
1673 // Records the invocation in this tracker.
1674 // This is needed for the recording mode only.
1675 // This assumes that the address matching is correctly performed before
1676 // calling this.
1677 // metering: free for recording
1678 #[cfg(any(test, feature = "recording_mode"))]
1679 fn record_invocation(
1680 &mut self,
1681 host: &Host,
1682 function: AuthorizedFunction,
1683 ) -> Result<(), HostError> {
1684 if self.current_frame_is_already_matched() {
1685 return Err(host.err(
1686 ScErrorType::Auth,
1687 ScErrorCode::ExistingValue,
1688 "frame is already authorized",
1689 &[],
1690 ));
1691 }
1692 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1693 curr_invocation
1694 .sub_invocations
1695 .push(AuthorizedInvocation::new_recording(function));
1696 let index_in_parent = curr_invocation.sub_invocations.len() - 1;
1697 *self.match_stack.last_mut().unwrap() = MatchState::SubMatch { index_in_parent };
1698 } else {
1699 // This would be a bug
1700 return Err(host.err(
1701 ScErrorType::Auth,
1702 ScErrorCode::InternalError,
1703 "unexpected missing authorized invocation",
1704 &[],
1705 ));
1706 }
1707 Ok(())
1708 }
1709
1710 // metering: free
1711 #[cfg(any(test, feature = "recording_mode"))]
1712 fn has_matched_invocations_in_stack(&self) -> bool {
1713 self.match_stack.iter().any(|i| i.is_matched())
1714 }
1715
1716 // metering: covered
1717 fn snapshot(&self, budget: &Budget) -> Result<AuthorizedInvocationSnapshot, HostError> {
1718 self.root_authorized_invocation.snapshot(budget)
1719 }
1720
1721 // metering: covered
1722 fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> {
1723 self.root_authorized_invocation.rollback(snapshot)?;
1724 // Invocation can only be rolled back from 'exhausted' to
1725 // 'non-exhausted' state (as there is no other way to go from
1726 // 'exhausted' state back to 'non-exhausted' state).
1727 if !self.root_authorized_invocation.is_exhausted {
1728 self.root_exhausted_frame = None;
1729 self.is_fully_processed = false;
1730 }
1731 Ok(())
1732 }
1733}
1734
1735impl AccountAuthorizationTracker {
1736 // Metering: covered by the host and components
1737 fn from_authorization_entry(
1738 host: &Host,
1739 auth_entry: SorobanAuthorizationEntry,
1740 ) -> Result<Self, HostError> {
1741 let (address, nonce, signature, is_transaction_source_account) =
1742 match auth_entry.credentials {
1743 SorobanCredentials::SourceAccount => (
1744 host.source_account_address()?.ok_or_else(|| {
1745 host.err(
1746 ScErrorType::Auth,
1747 ScErrorCode::InternalError,
1748 "source account is missing when setting auth entries",
1749 &[],
1750 )
1751 })?,
1752 None,
1753 Val::VOID.into(),
1754 true,
1755 ),
1756 SorobanCredentials::Address(address_creds) => (
1757 host.add_host_object(address_creds.address)?,
1758 Some((
1759 address_creds.nonce,
1760 address_creds.signature_expiration_ledger,
1761 )),
1762 host.to_host_val(&address_creds.signature)?,
1763 false,
1764 ),
1765 };
1766 Ok(Self {
1767 address,
1768 invocation_tracker: InvocationTracker::from_xdr(host, auth_entry.root_invocation)?,
1769 signature,
1770 verified: false,
1771 is_transaction_source_account,
1772 nonce,
1773 })
1774 }
1775
1776 // metering: free, since this is recording mode only
1777 #[cfg(any(test, feature = "recording_mode"))]
1778 fn new_recording(
1779 host: &Host,
1780 address: AddressObject,
1781 function: AuthorizedFunction,
1782 current_stack_len: usize,
1783 ) -> Result<Self, HostError> {
1784 if current_stack_len == 0 {
1785 // This would be a bug.
1786 return Err(host.err(
1787 ScErrorType::Auth,
1788 ScErrorCode::InternalError,
1789 "unexpected empty stack in recording auth",
1790 &[],
1791 ));
1792 }
1793 // Decide if we're tracking the transaction source account, and if so
1794 // don't bother with a nonce.
1795 let is_transaction_source_account =
1796 if let Some(source_acc) = host.source_account_address()? {
1797 host.compare(&source_acc, &address)?.is_eq()
1798 } else {
1799 false
1800 };
1801 let nonce = if !is_transaction_source_account {
1802 let random_nonce: i64 =
1803 host.with_recording_auth_nonce_prng(|p| Ok(p.gen_range(0..=i64::MAX)))?;
1804 // We use the `max_live_until_ledger` as the nonce lifetime here
1805 // in order to account for a maximum possible rent fee (given maximum
1806 // possible signature expiration). However, we don't want to actually
1807 // store that as nonce expiration ledger in the recording tracker,
1808 // as users are able (and encouraged) to customize the signature
1809 // expiration after simulation and before signing the auth payload.
1810 host.consume_nonce(address, random_nonce, host.max_live_until_ledger()?)?;
1811 Some((random_nonce, 0))
1812 } else {
1813 None
1814 };
1815 Ok(Self {
1816 address,
1817 invocation_tracker: InvocationTracker::new_recording(function, current_stack_len),
1818 signature: Val::VOID.into(),
1819 verified: true,
1820 is_transaction_source_account,
1821 nonce,
1822 })
1823 }
1824
1825 // Tries to find and enforce the provided invocation with this tracker and
1826 // lazily performs authentication when needed.
1827 // This is needed for the enforcing mode only.
1828 // This assumes that the address matching is correctly performed before
1829 // calling this.
1830 // Returns true/false based on whether the invocation is found in the
1831 // tracker. Returns error if invocation has been found, but the tracker
1832 // itself is not valid (failed authentication or nonce check).
1833 // metering: covered
1834 fn maybe_authorize_invocation(
1835 &mut self,
1836 host: &Host,
1837 function: &AuthorizedFunction,
1838 allow_matching_root: bool,
1839 ) -> Result<bool, HostError> {
1840 if !self.invocation_tracker.maybe_extend_invocation_match(
1841 host,
1842 function,
1843 allow_matching_root,
1844 )? {
1845 // The call isn't found in the currently tracked tree or is already
1846 // authorized in it.
1847 // That doesn't necessarily mean it's unauthorized (it can be
1848 // authorized in a different tracker).
1849 return Ok(false);
1850 }
1851 if !self.verified {
1852 let authenticate_res = self
1853 .authenticate(host)
1854 .map_err(|err| {
1855 // Convert any recoverable errors to auth errors so that it's
1856 // not possible to confuse them for the errors of the
1857 // contract that has called `require_auth`.
1858 // While there is no 'recovery' here, non-recoverable errors
1859 // aren't really useful for decoration.
1860 if err.is_recoverable() {
1861 // Also log the original error for diagnostics.
1862 host.err(
1863 ScErrorType::Auth,
1864 ScErrorCode::InvalidAction,
1865 "failed account authentication with error",
1866 &[self.address.into(), err.error.to_val()],
1867 )
1868 } else {
1869 err
1870 }
1871 })
1872 .and_then(|_| self.verify_and_consume_nonce(host));
1873 if let Some(err) = authenticate_res.err() {
1874 return Err(err);
1875 }
1876 self.verified = true;
1877 }
1878 Ok(true)
1879 }
1880
1881 // Records the invocation in this tracker.
1882 // This is needed for the recording mode only.
1883 // This assumes that the address matching is correctly performed before
1884 // calling this.
1885 // metering: free for recording
1886 #[cfg(any(test, feature = "recording_mode"))]
1887 fn record_invocation(
1888 &mut self,
1889 host: &Host,
1890 function: AuthorizedFunction,
1891 ) -> Result<(), HostError> {
1892 self.invocation_tracker.record_invocation(host, function)
1893 }
1894
1895 // Build the authorization payload from the invocations recorded in this
1896 // tracker.
1897 // metering: free for recording
1898 #[cfg(any(test, feature = "recording_mode"))]
1899 fn get_recorded_auth_payload(&self, host: &Host) -> Result<RecordedAuthPayload, HostError> {
1900 host.as_budget().with_observable_shadow_mode(|| {
1901 Ok(RecordedAuthPayload {
1902 address: if !self.is_transaction_source_account {
1903 Some(host.visit_obj(self.address, |a: &ScAddress| a.metered_clone(host))?)
1904 } else {
1905 None
1906 },
1907 invocation: self
1908 .invocation_tracker
1909 .root_authorized_invocation
1910 .to_xdr(host, false)?,
1911 nonce: self.nonce.map(|(nonce, _)| nonce),
1912 })
1913 })
1914 }
1915
1916 // Checks if there is at least one authorized invocation in the current call
1917 // stack.
1918 // metering: free
1919 #[cfg(any(test, feature = "recording_mode"))]
1920 fn has_authorized_invocations_in_stack(&self) -> bool {
1921 self.invocation_tracker.has_matched_invocations_in_stack()
1922 }
1923
1924 // metering: covered
1925 fn root_invocation_to_xdr(
1926 &self,
1927 host: &Host,
1928 ) -> Result<xdr::SorobanAuthorizedInvocation, HostError> {
1929 self.invocation_tracker
1930 .root_authorized_invocation
1931 .to_xdr(host, false)
1932 }
1933
1934 // metering: covered
1935 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1936 self.invocation_tracker.push_frame(budget)
1937 }
1938
1939 // metering: covered
1940 fn pop_frame(&mut self) {
1941 self.invocation_tracker.pop_frame();
1942 }
1943
1944 // metering: covered
1945 fn verify_and_consume_nonce(&mut self, host: &Host) -> Result<(), HostError> {
1946 if self.is_transaction_source_account {
1947 return Ok(());
1948 }
1949 if let Some((nonce, live_until_ledger)) = &self.nonce {
1950 let ledger_seq = host.with_ledger_info(|li| Ok(li.sequence_number))?;
1951 if ledger_seq > *live_until_ledger {
1952 return Err(host.err(
1953 ScErrorType::Auth,
1954 ScErrorCode::InvalidInput,
1955 "signature has expired",
1956 &[
1957 self.address.into(),
1958 ledger_seq.try_into_val(host)?,
1959 live_until_ledger.try_into_val(host)?,
1960 ],
1961 ));
1962 }
1963 let max_live_until_ledger = host.max_live_until_ledger()?;
1964 if *live_until_ledger > max_live_until_ledger {
1965 return Err(host.err(
1966 ScErrorType::Auth,
1967 ScErrorCode::InvalidInput,
1968 "signature expiration is too late",
1969 &[
1970 self.address.into(),
1971 max_live_until_ledger.try_into_val(host)?,
1972 live_until_ledger.try_into_val(host)?,
1973 ],
1974 ));
1975 }
1976
1977 return host.consume_nonce(self.address, *nonce, *live_until_ledger);
1978 }
1979 Err(host.err(
1980 ScErrorType::Auth,
1981 ScErrorCode::InternalError,
1982 "unexpected nonce verification state",
1983 &[],
1984 ))
1985 }
1986
1987 // Computes the payload that has to be signed in order to authenticate
1988 // the authorized invocation tree corresponding to this tracker.
1989 // metering: covered by components
1990 fn get_signature_payload(&self, host: &Host) -> Result<[u8; 32], HostError> {
1991 let (nonce, live_until_ledger) = self.nonce.ok_or_else(|| {
1992 host.err(
1993 ScErrorType::Auth,
1994 ScErrorCode::InternalError,
1995 "unexpected missing nonce",
1996 &[],
1997 )
1998 })?;
1999 let payload_preimage =
2000 HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
2001 network_id: Hash(host.with_ledger_info(|li| li.network_id.metered_clone(host))?),
2002 nonce,
2003 signature_expiration_ledger: live_until_ledger,
2004 invocation: self.root_invocation_to_xdr(host)?,
2005 });
2006
2007 host.metered_hash_xdr(&payload_preimage)
2008 }
2009
2010 // metering: covered by the hsot
2011 fn authenticate(&self, host: &Host) -> Result<(), HostError> {
2012 if self.is_transaction_source_account {
2013 return Ok(());
2014 }
2015
2016 let sc_addr = host.scaddress_from_address(self.address)?;
2017 // TODO: there should also be a mode where a dummy payload is used
2018 // instead (for enforcing mode preflight).
2019 let payload = self.get_signature_payload(host)?;
2020 match sc_addr {
2021 ScAddress::Account(acc) => {
2022 check_account_authentication(host, acc, &payload, self.signature)?;
2023 }
2024 ScAddress::Contract(acc_contract) => {
2025 check_account_contract_auth(
2026 host,
2027 &acc_contract,
2028 &payload,
2029 self.signature,
2030 &self.invocation_tracker.root_authorized_invocation,
2031 )?;
2032 }
2033 }
2034 Ok(())
2035 }
2036
2037 // Emulates authentication for the recording mode.
2038 // metering: covered
2039 #[cfg(any(test, feature = "recording_mode"))]
2040 fn emulate_authentication(&mut self, host: &Host) -> Result<(), HostError> {
2041 if self.is_transaction_source_account {
2042 return Ok(());
2043 }
2044 let sc_addr = host.scaddress_from_address(self.address)?;
2045 match sc_addr {
2046 ScAddress::Account(acc) => {
2047 // Emulate verification of a single signature that belongs to this
2048 // account.
2049 // We could emulate more (up to 20) signature verifications, but
2050 // since signature verification is a pretty expensive operation, while
2051 // multisig in combination with Soroban auth is probably pretty rare,
2052 // multisig users should either use enforcing auth simulation, or
2053 // intentionally increase the instruction count on the recording result.
2054 let key_bytes = match &acc.0 {
2055 PublicKey::PublicKeyTypeEd25519(k) => k.0,
2056 };
2057 let signature = AccountEd25519Signature {
2058 public_key: BytesN::from_slice(host, &key_bytes)?,
2059 signature: BytesN::from_slice(host, &[0_u8; 64])?,
2060 };
2061 let signatures = host_vec![host, signature]?;
2062 self.signature = signatures.into();
2063 // Authentication is expected to fail here after signature verification,
2064 // so we suppress the error and diagnostics.
2065 host.with_suppressed_diagnostic_events(|| {
2066 let _ = self.authenticate(host);
2067 Ok(())
2068 })?;
2069
2070 // Emulate a clone of the account, which serves 2 purposes:
2071 // - Account for metered clone in `get_signer_weight_from_account`
2072 // - Return budget error in case if it was suppressed above.
2073 let _ = acc.metered_clone(host.as_budget())?;
2074 }
2075 // We only know for sure that the contract instance and Wasm will be
2076 // loaded.
2077 ScAddress::Contract(contract_id) => {
2078 let instance_key = host.contract_instance_ledger_key(&contract_id)?;
2079 let entry = host
2080 .try_borrow_storage_mut()?
2081 .try_get(&instance_key, host, None)?;
2082 // In test scenarios we often may not have any actual instance, which is fine most
2083 // of the time, so we don't return any errors.
2084 // In simulation scenarios the instance will likely be there, and when it's
2085 // not, we still make our best effort and include at least the necessary instance key
2086 // into the footprint.
2087 let instance = if let Some(entry) = entry {
2088 match &entry.data {
2089 LedgerEntryData::ContractData(e) => match &e.val {
2090 ScVal::ContractInstance(instance) => instance.metered_clone(host)?,
2091 _ => {
2092 return Ok(());
2093 }
2094 },
2095 _ => {
2096 return Ok(());
2097 }
2098 }
2099 } else {
2100 return Ok(());
2101 };
2102
2103 match &instance.executable {
2104 ContractExecutable::Wasm(wasm_hash) => {
2105 let wasm_key = host.contract_code_ledger_key(wasm_hash)?;
2106 let _ = host
2107 .try_borrow_storage_mut()?
2108 .try_get(&wasm_key, host, None)?;
2109 }
2110 ContractExecutable::StellarAsset => (),
2111 }
2112 }
2113 }
2114 Ok(())
2115 }
2116
2117 // metering: covered
2118 fn snapshot(&self, budget: &Budget) -> Result<AccountAuthorizationTrackerSnapshot, HostError> {
2119 Ok(AccountAuthorizationTrackerSnapshot {
2120 invocation_tracker_root_snapshot: self.invocation_tracker.snapshot(budget)?,
2121 // The verification status can be rolled back in case
2122 // of a contract failure. In case if the root call has
2123 // failed, the nonce will get 'un-consumed' due to storage
2124 // rollback. Thus we need to run verification and consume
2125 // it again in case if the call is retried. Another subtle
2126 // case where this behavior is important is the case when
2127 // custom account's authentication function depends on
2128 // some ledger state which might get modified in-between calls.
2129 verified: self.verified,
2130 })
2131 }
2132
2133 // metering: covered
2134 fn rollback(
2135 &mut self,
2136 snapshot: &AccountAuthorizationTrackerSnapshot,
2137 ) -> Result<(), HostError> {
2138 self.invocation_tracker
2139 .rollback(&snapshot.invocation_tracker_root_snapshot)?;
2140 self.verified = snapshot.verified;
2141 Ok(())
2142 }
2143
2144 // metering: free
2145 fn is_active(&self) -> bool {
2146 self.invocation_tracker.is_active()
2147 }
2148
2149 // metering: free
2150 fn current_frame_is_already_matched(&self) -> bool {
2151 self.invocation_tracker.current_frame_is_already_matched()
2152 }
2153}
2154
2155impl InvokerContractAuthorizationTracker {
2156 // metering: covered by components
2157 fn new_with_curr_contract_as_invoker(
2158 host: &Host,
2159 invoker_auth_entry: Val,
2160 ) -> Result<Self, HostError> {
2161 let invoker_sc_addr = ScAddress::Contract(host.get_current_contract_id_internal()?);
2162 let authorized_invocation = invoker_contract_auth_to_authorized_invocation(
2163 host,
2164 &invoker_sc_addr,
2165 invoker_auth_entry,
2166 )?;
2167 let invocation_tracker = InvocationTracker::new(authorized_invocation);
2168 Ok(Self {
2169 contract_address: host.add_host_object(invoker_sc_addr)?,
2170 invocation_tracker,
2171 })
2172 }
2173
2174 // metering: covered
2175 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
2176 self.invocation_tracker.push_frame(budget)
2177 }
2178
2179 // metering: covered
2180 fn pop_frame(&mut self) {
2181 self.invocation_tracker.pop_frame();
2182 }
2183
2184 // metering: free
2185 fn is_out_of_scope(&self) -> bool {
2186 self.invocation_tracker.is_empty()
2187 }
2188
2189 // metering: covered
2190 fn maybe_authorize_invocation(
2191 &mut self,
2192 host: &Host,
2193 function: &AuthorizedFunction,
2194 ) -> Result<bool, HostError> {
2195 // Authorization is successful if function is just matched by the
2196 // tracker. No authentication is needed.
2197 self.invocation_tracker
2198 .maybe_extend_invocation_match(host, function, true)
2199 }
2200}
2201
2202impl Host {
2203 // metering: covered by components
2204 fn consume_nonce(
2205 &self,
2206 address: AddressObject,
2207 nonce: i64,
2208 live_until_ledger: u32,
2209 ) -> Result<(), HostError> {
2210 let nonce_key_scval = ScVal::LedgerKeyNonce(ScNonceKey { nonce });
2211 let sc_address = self.scaddress_from_address(address)?;
2212 let nonce_key = self.storage_key_for_address(
2213 sc_address.metered_clone(self)?,
2214 nonce_key_scval.metered_clone(self)?,
2215 xdr::ContractDataDurability::Temporary,
2216 )?;
2217 let live_until_ledger = live_until_ledger
2218 .max(self.get_min_live_until_ledger(xdr::ContractDataDurability::Temporary)?);
2219 self.with_mut_storage(|storage| {
2220 if storage
2221 .has_with_host(&nonce_key, self, None)
2222 .map_err(|err| {
2223 if err.error.is_type(ScErrorType::Storage)
2224 && err.error.is_code(ScErrorCode::ExceededLimit)
2225 {
2226 return self.err(
2227 ScErrorType::Storage,
2228 ScErrorCode::ExceededLimit,
2229 "trying to access nonce outside of footprint for address",
2230 &[address.to_val()],
2231 );
2232 }
2233 err
2234 })?
2235 {
2236 return Err(self.err(
2237 ScErrorType::Auth,
2238 ScErrorCode::ExistingValue,
2239 "nonce already exists for address",
2240 &[address.into()],
2241 ));
2242 }
2243 let data = LedgerEntryData::ContractData(ContractDataEntry {
2244 contract: sc_address,
2245 key: nonce_key_scval,
2246 val: ScVal::Void,
2247 durability: xdr::ContractDataDurability::Temporary,
2248 ext: xdr::ExtensionPoint::V0,
2249 });
2250 let entry = LedgerEntry {
2251 last_modified_ledger_seq: 0,
2252 data,
2253 ext: LedgerEntryExt::V0,
2254 };
2255 storage.put_with_host(
2256 &nonce_key,
2257 &Rc::metered_new(entry, self)?,
2258 Some(live_until_ledger),
2259 self,
2260 None,
2261 )
2262 })
2263 }
2264
2265 // Returns the recorded per-address authorization payloads that would cover the
2266 // top-level contract function invocation in the enforcing mode.
2267 // This should only be called in the recording authorization mode, i.e. only
2268 // if `switch_to_recording_auth` has been called.
2269 #[cfg(any(test, feature = "recording_mode"))]
2270 pub fn get_recorded_auth_payloads(&self) -> Result<Vec<RecordedAuthPayload>, HostError> {
2271 #[cfg(not(any(test, feature = "testutils")))]
2272 {
2273 self.try_borrow_authorization_manager()?
2274 .get_recorded_auth_payloads(self)
2275 }
2276 #[cfg(any(test, feature = "testutils"))]
2277 {
2278 let payloads = self
2279 .try_borrow_previous_authorization_manager()?
2280 .as_ref()
2281 .ok_or_else(|| {
2282 self.err(
2283 ScErrorType::Auth,
2284 ScErrorCode::InvalidAction,
2285 "previous invocation is missing - no auth data to get",
2286 &[],
2287 )
2288 })?
2289 .get_recorded_auth_payloads(self)?;
2290 Ok(payloads)
2291 }
2292 }
2293}
2294
2295#[cfg(any(test, feature = "testutils"))]
2296use crate::{host::frame::CallParams, xdr::SorobanAuthorizedInvocation};
2297
2298#[cfg(any(test, feature = "testutils"))]
2299impl Host {
2300 /// Invokes the reserved `__check_auth` function on a provided contract.
2301 ///
2302 /// This is useful for testing the custom account contracts. Otherwise, the
2303 /// host prohibits calling `__check_auth` outside of internal implementation
2304 /// of `require_auth[_for_args]` calls.
2305 pub fn call_account_contract_check_auth(
2306 &self,
2307 contract: AddressObject,
2308 args: VecObject,
2309 ) -> Result<Val, HostError> {
2310 let _invocation_meter_scope = self.maybe_meter_invocation()?;
2311
2312 use crate::builtin_contracts::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME;
2313 let contract_id = self.contract_id_from_address(contract)?;
2314 let args_vec = self.call_args_from_obj(args)?;
2315 let res = self.call_n_internal(
2316 &contract_id,
2317 ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(self)?,
2318 args_vec.as_slice(),
2319 CallParams::default_internal_call(),
2320 );
2321 if let Err(e) = &res {
2322 self.error(
2323 e.error,
2324 "check auth invocation for a custom account contract failed",
2325 &[contract.to_val(), args.to_val()],
2326 );
2327 }
2328 res
2329 }
2330
2331 /// Returns the current state of the authorization manager.
2332 ///
2333 /// Use this in conjunction with `set_auth_manager` to do authorized
2334 /// operations without breaking the current authorization state (useful for
2335 /// preserving the auth state while doing the generic test setup).
2336 pub fn snapshot_auth_manager(&self) -> Result<AuthorizationManager, HostError> {
2337 Ok(self.try_borrow_authorization_manager()?.clone())
2338 }
2339
2340 /// Switches host to the recording authorization mode and inherits the
2341 /// recording mode settings from the provided authorization manager settings
2342 /// in case if it used the recording mode.
2343 ///
2344 /// This is similar to `switch_to_recording_auth`, but should be preferred
2345 /// to use in conjunction with `snapshot_auth_manager`, such that the
2346 /// recording mode settings are not overridden.
2347 pub fn switch_to_recording_auth_inherited_from_snapshot(
2348 &self,
2349 auth_manager_snapshot: &AuthorizationManager,
2350 ) -> Result<(), HostError> {
2351 let disable_non_root_auth = match &auth_manager_snapshot.mode {
2352 AuthorizationMode::Enforcing => true,
2353 AuthorizationMode::Recording(recording_auth_info) => {
2354 recording_auth_info.disable_non_root_auth
2355 }
2356 };
2357 *self.try_borrow_authorization_manager_mut()? =
2358 AuthorizationManager::new_recording(disable_non_root_auth);
2359 Ok(())
2360 }
2361
2362 /// Replaces authorization manager with the provided new instance.
2363 ///
2364 /// Use this in conjunction with `snapshot_auth_manager` to do authorized
2365 /// operations without breaking the current authorization state (useful for
2366 /// preserving the auth state while doing the generic test setup).
2367 pub fn set_auth_manager(&self, auth_manager: AuthorizationManager) -> Result<(), HostError> {
2368 *self.try_borrow_authorization_manager_mut()? = auth_manager;
2369 Ok(())
2370 }
2371
2372 // Returns the authorizations that have been authenticated for the last
2373 // contract invocation.
2374 //
2375 // Authenticated means that either the authorization was authenticated using
2376 // the actual authorization logic for that authorization in enforced mode,
2377 // or that it was recorded in recording mode and authorization was assumed
2378 // successful.
2379 pub fn get_authenticated_authorizations(
2380 &self,
2381 ) -> Result<Vec<(ScAddress, SorobanAuthorizedInvocation)>, HostError> {
2382 Ok(self
2383 .try_borrow_previous_authorization_manager_mut()?
2384 .as_mut()
2385 .map(|am| am.get_authenticated_authorizations(self))
2386 // If no AuthorizationManager is setup, no authorizations could have
2387 // taken place so return an empty vec.
2388 .unwrap_or_default())
2389 }
2390}
2391
2392// metering: free for testutils
2393#[cfg(any(test, feature = "testutils"))]
2394impl PartialEq for RecordedAuthPayload {
2395 fn eq(&self, other: &Self) -> bool {
2396 self.address == other.address
2397 && self.invocation == other.invocation
2398 // Compare nonces only by presence of the value - recording mode
2399 // generates random nonces.
2400 && self.nonce.is_some() == other.nonce.is_some()
2401 }
2402}