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 is_invoker_contract(&self, host: &Host, address: AddressObject) -> Result<bool, HostError> {
990 let call_stack = self.try_borrow_call_stack(host)?;
991 for i in 0..(call_stack.len() - 1) {
992 match call_stack[i] {
993 AuthStackFrame::CreateContractHostFn(_) => (),
994 AuthStackFrame::Contract(ref contract_frame) => {
995 if host
996 .compare(&contract_frame.contract_address, &address)?
997 .is_eq()
998 {
999 return Ok(true);
1000 }
1001 }
1002 }
1003 }
1004 Ok(false)
1005 }
1006
1007 #[cfg(any(test, feature = "recording_mode"))]
1008 fn require_auth_recording(
1009 &self,
1010 host: &Host,
1011 address: AddressObject,
1012 function: AuthorizedFunction,
1013 recording_info: &RecordingAuthInfo,
1014 ) -> Result<(), HostError> {
1015 // At first, try to find the tracker for this exact address
1016 // object.
1017 // This is a best-effort heuristic to come up with a reasonably
1018 // looking recording tree for cases when multiple instances of
1019 // the same exact address are used.
1020 let address_obj_handle = address.get_handle();
1021 let existing_tracker_id = recording_info
1022 .try_borrow_tracker_by_address_handle(host)?
1023 .get(&address_obj_handle)
1024 .copied();
1025 if let Some(tracker_id) = existing_tracker_id {
1026 // The tracker should not be borrowed recursively in
1027 // recording mode, as we don't call `__check_auth` in this
1028 // flow.
1029 let trackers = self.try_borrow_account_trackers(host)?;
1030 let Some(trackercell) = trackers.get(tracker_id) else {
1031 return Err(host.err(
1032 ScErrorType::Auth,
1033 ScErrorCode::InternalError,
1034 "bad index for existing tracker",
1035 &[],
1036 ));
1037 };
1038 if let Ok(mut tracker) = trackercell.try_borrow_mut() {
1039 // The recording invariant is that trackers are created
1040 // with the first authorized invocation, which means
1041 // that when their stack no longer has authorized
1042 // invocation, then we've popped frames past its root
1043 // and hence need to create a new tracker.
1044 if !tracker.has_authorized_invocations_in_stack() {
1045 recording_info
1046 .try_borrow_tracker_by_address_handle_mut(host)?
1047 .remove(&address_obj_handle);
1048 } else {
1049 return tracker.record_invocation(host, function);
1050 }
1051 } else {
1052 return Err(host.err(
1053 ScErrorType::Auth,
1054 ScErrorCode::InternalError,
1055 "unexpected recursive tracker borrow in recording mode",
1056 &[],
1057 ));
1058 };
1059 }
1060 // If there is no active tracker for this exact address object,
1061 // try to find any matching active tracker for the address.
1062 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1063 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1064 if !host.compare(&tracker.address, &address)?.is_eq() {
1065 continue;
1066 }
1067 // Take the first tracker that is still active (i.e. has
1068 // active authorizations in the current call stack) and
1069 // hasn't been used for this stack frame yet.
1070 if tracker.has_authorized_invocations_in_stack()
1071 && !tracker.current_frame_is_already_matched()
1072 {
1073 return tracker.record_invocation(host, function);
1074 }
1075 } else {
1076 return Err(host.err(
1077 ScErrorType::Auth,
1078 ScErrorCode::InternalError,
1079 "unexpected borrowed tracker in recording auth mode",
1080 &[],
1081 ));
1082 }
1083 }
1084 // At this stage there is no active tracker to which we could
1085 // match the current invocation, thus we need to create a new
1086 // tracker.
1087 // Alert the user in `disable_non_root_auth` mode if we're not
1088 // in the root stack frame.
1089 if recording_info.disable_non_root_auth && self.try_borrow_call_stack(host)?.len() != 1 {
1090 if self.is_invoker_contract(host, address)? {
1091 return Err(host.err(
1092 ScErrorType::Auth,
1093 ScErrorCode::InvalidAction,
1094 "[recording authorization only] encountered unauthorized call for a \
1095 contract earlier in the call stack, make sure that you have called \
1096 `authorize_as_current_contract()` with the appropriate arguments for it.",
1097 &[address.into()],
1098 ));
1099 }
1100 return Err(host.err(
1101 ScErrorType::Auth,
1102 ScErrorCode::InvalidAction,
1103 "[recording authorization only] encountered authorization not tied \
1104 to the root contract invocation for an address. Use `require_auth()` \
1105 in the top invocation or enable non-root authorization.",
1106 &[address.into()],
1107 ));
1108 }
1109 // If a tracker for the new tree doesn't exist yet, create
1110 // it and initialize with the current invocation.
1111 self.try_borrow_account_trackers_mut(host)?
1112 .push(RefCell::new(AccountAuthorizationTracker::new_recording(
1113 host,
1114 address,
1115 function,
1116 self.try_borrow_call_stack(host)?.len(),
1117 )?));
1118 recording_info
1119 .try_borrow_tracker_by_address_handle_mut(host)?
1120 .insert(
1121 address_obj_handle,
1122 self.try_borrow_account_trackers(host)?.len() - 1,
1123 );
1124 Ok(())
1125 }
1126
1127 // metering: covered
1128 fn require_auth_internal(
1129 &self,
1130 host: &Host,
1131 address: AddressObject,
1132 function: AuthorizedFunction,
1133 ) -> Result<(), HostError> {
1134 // First check the InvokerContractAuthorizationTrackers
1135 if self.maybe_check_invoker_contract_auth(host, address, &function)? {
1136 return Ok(());
1137 }
1138 // Then check the AccountAuthorizationTrackers
1139 match &self.mode {
1140 AuthorizationMode::Enforcing => self.require_auth_enforcing(host, address, &function),
1141 // metering: free for recording
1142 #[cfg(any(test, feature = "recording_mode"))]
1143 AuthorizationMode::Recording(recording_info) => {
1144 self.require_auth_recording(host, address, function, recording_info)
1145 }
1146 }
1147 }
1148
1149 // Returns a snapshot of `AuthorizationManager` to use for rollback.
1150 // metering: covered
1151 fn snapshot(&self, host: &Host) -> Result<AuthorizationManagerSnapshot, HostError> {
1152 let _span = tracy_span!("snapshot auth");
1153 let account_trackers_snapshot = match &self.mode {
1154 AuthorizationMode::Enforcing => {
1155 let len = self.try_borrow_account_trackers(host)?.len();
1156 let mut snapshots =
1157 Vec::<Option<AccountAuthorizationTrackerSnapshot>>::with_metered_capacity(
1158 len, host,
1159 )?;
1160 for t in self.try_borrow_account_trackers(host)?.iter() {
1161 let sp = if let Ok(tracker) = t.try_borrow() {
1162 Some(tracker.snapshot(host.as_budget())?)
1163 } else {
1164 // If tracker is borrowed, snapshotting it is a no-op
1165 // (it can't change until we release it higher up the
1166 // stack).
1167 None
1168 };
1169 snapshots.push(sp);
1170 }
1171 AccountTrackersSnapshot::Enforcing(snapshots)
1172 }
1173 #[cfg(any(test, feature = "recording_mode"))]
1174 AuthorizationMode::Recording(_) => {
1175 // All trackers should be available to borrow for copy as in
1176 // recording mode we can't have recursive authorization.
1177 // metering: free for recording
1178 AccountTrackersSnapshot::Recording(self.try_borrow_account_trackers(host)?.clone())
1179 }
1180 };
1181 let invoker_contract_tracker_root_snapshots = self
1182 .try_borrow_invoker_contract_trackers(host)?
1183 .iter()
1184 .map(|t| t.invocation_tracker.snapshot(host.as_budget()))
1185 .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(host)??;
1186 #[cfg(any(test, feature = "recording_mode"))]
1187 let tracker_by_address_handle = match &self.mode {
1188 AuthorizationMode::Enforcing => None,
1189 AuthorizationMode::Recording(recording_info) => Some(
1190 // metering: free for recording
1191 recording_info
1192 .try_borrow_tracker_by_address_handle(host)?
1193 .clone(),
1194 ),
1195 };
1196 Ok(AuthorizationManagerSnapshot {
1197 account_trackers_snapshot,
1198 invoker_contract_tracker_root_snapshots,
1199 #[cfg(any(test, feature = "recording_mode"))]
1200 tracker_by_address_handle,
1201 })
1202 }
1203
1204 // Rolls back this `AuthorizationManager` to the snapshot state.
1205 // metering: covered
1206 fn rollback(
1207 &self,
1208 host: &Host,
1209 snapshot: AuthorizationManagerSnapshot,
1210 ) -> Result<(), HostError> {
1211 let _span = tracy_span!("rollback auth");
1212 match snapshot.account_trackers_snapshot {
1213 AccountTrackersSnapshot::Enforcing(trackers_snapshot) => {
1214 let trackers = self.try_borrow_account_trackers(host)?;
1215 if trackers.len() != trackers_snapshot.len() {
1216 return Err(host.err(
1217 ScErrorType::Auth,
1218 ScErrorCode::InternalError,
1219 "unexpected bad auth snapshot",
1220 &[],
1221 ));
1222 }
1223 for (i, tracker) in trackers.iter().enumerate() {
1224 let Some(snapopt) = trackers_snapshot.get(i) else {
1225 return Err(host.err(
1226 ScErrorType::Auth,
1227 ScErrorCode::InternalError,
1228 "unexpected auth snapshot index",
1229 &[],
1230 ));
1231 };
1232 if let Some(tracker_snapshot) = snapopt {
1233 tracker
1234 .try_borrow_mut()
1235 .map_err(|_| {
1236 host.err(
1237 ScErrorType::Auth,
1238 ScErrorCode::InternalError,
1239 "unexpected bad auth borrow",
1240 &[],
1241 )
1242 })?
1243 .rollback(&tracker_snapshot)?;
1244 }
1245 }
1246 }
1247 #[cfg(any(test, feature = "recording_mode"))]
1248 AccountTrackersSnapshot::Recording(s) => {
1249 *self.try_borrow_account_trackers_mut(host)? = s;
1250 }
1251 }
1252
1253 let mut invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1254
1255 if invoker_trackers.len() < snapshot.invoker_contract_tracker_root_snapshots.len() {
1256 return Err(host.err(
1257 ScErrorType::Auth,
1258 ScErrorCode::InternalError,
1259 "the number of invoker contract trackers is smaller than in the snapshot",
1260 &[],
1261 ));
1262 }
1263 // If there are more trackers than in the snapshot, then the trackers have been
1264 // created in the current (failed) frame, so we should remove them as a part
1265 // of rollback.
1266 invoker_trackers.truncate(snapshot.invoker_contract_tracker_root_snapshots.len());
1267
1268 for (tracker, snapshot) in invoker_trackers
1269 .iter_mut()
1270 .zip(snapshot.invoker_contract_tracker_root_snapshots.iter())
1271 {
1272 tracker.invocation_tracker.rollback(snapshot)?;
1273 }
1274
1275 #[cfg(any(test, feature = "recording_mode"))]
1276 if let Some(tracker_by_address_handle) = snapshot.tracker_by_address_handle {
1277 match &self.mode {
1278 AuthorizationMode::Enforcing => (),
1279 AuthorizationMode::Recording(recording_info) => {
1280 *recording_info.try_borrow_tracker_by_address_handle_mut(host)? =
1281 tracker_by_address_handle;
1282 }
1283 }
1284 }
1285 Ok(())
1286 }
1287
1288 // metering: covered
1289 fn push_tracker_frame(&self, host: &Host) -> Result<(), HostError> {
1290 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1291 // Skip already borrowed trackers, these must be in the middle of
1292 // authentication and hence don't need stack to be updated.
1293 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1294 tracker.push_frame(host.as_budget())?;
1295 }
1296 }
1297 for tracker in self
1298 .try_borrow_invoker_contract_trackers_mut(host)?
1299 .iter_mut()
1300 {
1301 tracker.push_frame(host.as_budget())?;
1302 }
1303 Ok(())
1304 }
1305
1306 // metering: covered
1307 pub(crate) fn push_create_contract_host_fn_frame(
1308 &self,
1309 host: &Host,
1310 args: CreateContractArgsV2,
1311 ) -> Result<(), HostError> {
1312 Vec::<CreateContractArgsV2>::charge_bulk_init_cpy(1, host)?;
1313 self.try_borrow_call_stack_mut(host)?
1314 .push(AuthStackFrame::CreateContractHostFn(args));
1315 self.push_tracker_frame(host)
1316 }
1317
1318 // Records a new call stack frame and returns a snapshot for rolling
1319 // back this stack frame.
1320 // This should be called for every `Host` `push_frame`.
1321 // metering: covered
1322 pub(crate) fn push_frame(
1323 &self,
1324 host: &Host,
1325 frame: &Frame,
1326 ) -> Result<AuthorizationManagerSnapshot, HostError> {
1327 let _span = tracy_span!("push auth frame");
1328 let (contract_id, function_name) = match frame {
1329 Frame::ContractVM { vm, fn_name, .. } => {
1330 (vm.contract_id.metered_clone(host)?, *fn_name)
1331 }
1332 // Skip the top-level host function stack frames as they don't
1333 // contain all the necessary information.
1334 // Use the respective push (like
1335 // `push_create_contract_host_fn_frame`) functions instead to push
1336 // the frame with the required info.
1337 Frame::HostFunction(_) => return self.snapshot(host),
1338 Frame::StellarAssetContract(id, fn_name, ..) => (id.metered_clone(host)?, *fn_name),
1339 #[cfg(any(test, feature = "testutils"))]
1340 Frame::TestContract(tc) => (tc.id.metered_clone(host)?, tc.func),
1341 };
1342 let contract_address = host.add_host_object(ScAddress::Contract(contract_id))?;
1343 Vec::<ContractInvocation>::charge_bulk_init_cpy(1, host)?;
1344 self.try_borrow_call_stack_mut(host)?
1345 .push(AuthStackFrame::Contract(ContractInvocation {
1346 contract_address,
1347 function_name,
1348 }));
1349
1350 self.push_tracker_frame(host)?;
1351 self.snapshot(host)
1352 }
1353
1354 // Pops a call stack frame and maybe rolls back the internal
1355 // state according to the provided snapshot.
1356 // This should be called for every `Host` `pop_frame`.
1357 // metering: covered
1358 pub(crate) fn pop_frame(
1359 &self,
1360 host: &Host,
1361 snapshot: Option<AuthorizationManagerSnapshot>,
1362 ) -> Result<(), HostError> {
1363 let _span = tracy_span!("pop auth frame");
1364 // Important: rollback has to be performed before popping the frame
1365 // from the tracker. This ensures correct work of invoker contract
1366 // trackers for which snapshots are dependent on the current
1367 // call stack.
1368 if let Some(snapshot) = snapshot {
1369 self.rollback(host, snapshot)?;
1370 }
1371 {
1372 let mut call_stack = self.try_borrow_call_stack_mut(host)?;
1373 // Currently we don't push host function call frames, hence this may be
1374 // called with empty stack. We trust the Host to keep things correct,
1375 // i.e. that only host function frames are ignored this way.
1376 if call_stack.is_empty() {
1377 return Ok(());
1378 }
1379 call_stack.pop();
1380 }
1381 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1382 // Skip already borrowed trackers, these must be in the middle of
1383 // authentication and hence don't need stack to be updated.
1384 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1385 tracker.pop_frame();
1386 }
1387 }
1388
1389 let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1390 for tracker in invoker_contract_trackers.iter_mut() {
1391 tracker.pop_frame();
1392 }
1393 // Pop invoker contract trackers that went out of scope. The invariant
1394 // is that tracker only exists for the next sub-contract call (or until
1395 // the tracker's frame itself is popped). Thus trackers form a stack
1396 // where the shorter lifetime trackers are at the top.
1397 while let Some(last) = invoker_contract_trackers.last() {
1398 // *Subtle*: there are two possible scenarios when tracker is considered
1399 // to be out of scope:
1400 // - When the sub-contract call is finished. For example, contract A creates
1401 // a tracker, then calls contract B. When we pop the frame of contract B,
1402 // the tracker's stack will be empty and thus considered to be out of scope.
1403 // - When the contract that created the tracker is going out of scope without
1404 // calling any sub-contracts. For example, contract A creates a tracker and
1405 // returns. When we pop the frame of contract A, the tracker's stack will be
1406 // empty (because it's created empty), and thus considered to be out of scope.
1407 // The invariant above is maintained in both scenarios.
1408 if last.is_out_of_scope() {
1409 invoker_contract_trackers.pop();
1410 } else {
1411 break;
1412 }
1413 }
1414 Ok(())
1415 }
1416
1417 // Returns the recorded per-address authorization payloads that would cover the
1418 // top-level contract function invocation in the enforcing mode.
1419 // Should only be called in the recording mode.
1420 // metering: free, recording mode
1421 #[cfg(any(test, feature = "recording_mode"))]
1422 pub(crate) fn get_recorded_auth_payloads(
1423 &self,
1424 host: &Host,
1425 ) -> Result<Vec<RecordedAuthPayload>, HostError> {
1426 match &self.mode {
1427 AuthorizationMode::Enforcing => Err(HostError::from((
1428 ScErrorType::Auth,
1429 ScErrorCode::InternalError,
1430 ))),
1431 AuthorizationMode::Recording(_) => Ok(self
1432 .try_borrow_account_trackers(host)?
1433 .iter()
1434 .map(|tracker| tracker.try_borrow_or_err()?.get_recorded_auth_payload(host))
1435 .collect::<Result<Vec<RecordedAuthPayload>, HostError>>()?),
1436 }
1437 }
1438
1439 // For recording mode, emulates authentication that would normally happen in
1440 // the enforcing mode.
1441 // This helps to build a more realistic footprint and produce more correct
1442 // meterting data for the recording mode.
1443 // No-op in the enforcing mode.
1444 // metering: covered
1445 #[cfg(any(test, feature = "recording_mode"))]
1446 pub(crate) fn maybe_emulate_authentication(&self, host: &Host) -> Result<(), HostError> {
1447 match &self.mode {
1448 AuthorizationMode::Enforcing => Ok(()),
1449 AuthorizationMode::Recording(_) => {
1450 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1451 tracker
1452 .try_borrow_mut_or_err()?
1453 .emulate_authentication(host)?;
1454 }
1455 Ok(())
1456 }
1457 }
1458 }
1459
1460 // Returns a 'reset' instance of `AuthorizationManager` that has the same
1461 // mode, but no data.
1462 // metering: free, testutils
1463 #[cfg(any(test, feature = "testutils"))]
1464 pub(crate) fn reset(&mut self) {
1465 *self = match &self.mode {
1466 AuthorizationMode::Enforcing => {
1467 AuthorizationManager::new_enforcing_without_authorizations()
1468 }
1469 AuthorizationMode::Recording(rec_info) => {
1470 AuthorizationManager::new_recording(rec_info.disable_non_root_auth)
1471 }
1472 }
1473 }
1474
1475 // Returns all authorizations that have been authenticated for the
1476 // last contract invocation.
1477 // metering: free, testutils
1478 #[cfg(any(test, feature = "testutils"))]
1479 pub(crate) fn get_authenticated_authorizations(
1480 &self,
1481 host: &Host,
1482 ) -> Vec<(ScAddress, xdr::SorobanAuthorizedInvocation)> {
1483 host.as_budget()
1484 .with_observable_shadow_mode(|| {
1485 self.account_trackers
1486 .borrow()
1487 .iter()
1488 .filter(|t| t.borrow().verified)
1489 .map(|t| {
1490 (
1491 host.scaddress_from_address(t.borrow().address).unwrap(),
1492 t.borrow()
1493 .invocation_tracker
1494 .root_authorized_invocation
1495 .to_xdr(host, true)
1496 .unwrap(),
1497 )
1498 })
1499 .metered_collect(host)
1500 })
1501 .unwrap()
1502 }
1503}
1504
1505// Some helper extensions to support test-observation.
1506#[allow(dead_code)]
1507impl AuthorizationManager {
1508 pub(crate) fn stack_size(&self) -> usize {
1509 if let Ok(call_stack) = self.call_stack.try_borrow() {
1510 call_stack.len()
1511 } else {
1512 0
1513 }
1514 }
1515
1516 pub(crate) fn stack_hash(&self, budget: &Budget) -> Result<u64, HostError> {
1517 use std::hash::Hasher;
1518 if let Ok(call_stack) = self.call_stack.try_borrow() {
1519 let mut state = CountingHasher::default();
1520 call_stack.metered_hash(&mut state, budget)?;
1521 Ok(state.finish())
1522 } else {
1523 Ok(0)
1524 }
1525 }
1526
1527 pub(crate) fn trackers_hash_and_size(
1528 &self,
1529 budget: &Budget,
1530 ) -> Result<(u64, usize), HostError> {
1531 use std::hash::Hasher;
1532 let mut size: usize = 0;
1533 let mut state = CountingHasher::default();
1534 self.mode.metered_hash(&mut state, budget)?;
1535 if let Ok(account_trackers) = self.account_trackers.try_borrow() {
1536 for tracker in account_trackers.iter() {
1537 if let Ok(tracker) = tracker.try_borrow() {
1538 size = size.saturating_add(1);
1539 tracker.metered_hash(&mut state, budget)?;
1540 }
1541 }
1542 }
1543 if let Ok(invoker_contract_trackers) = self.invoker_contract_trackers.try_borrow() {
1544 for tracker in invoker_contract_trackers.iter() {
1545 size = size.saturating_add(1);
1546 tracker.metered_hash(&mut state, budget)?;
1547 }
1548 }
1549 Ok((state.finish(), size))
1550 }
1551}
1552
1553impl InvocationTracker {
1554 // metering: covered by components
1555 fn from_xdr(
1556 host: &Host,
1557 root_invocation: xdr::SorobanAuthorizedInvocation,
1558 ) -> Result<Self, HostError> {
1559 Ok(Self {
1560 root_authorized_invocation: AuthorizedInvocation::from_xdr(host, root_invocation)?,
1561 match_stack: vec![],
1562 root_exhausted_frame: None,
1563 is_fully_processed: false,
1564 })
1565 }
1566
1567 // metering: free
1568 fn new(root_authorized_invocation: AuthorizedInvocation) -> Self {
1569 Self {
1570 root_authorized_invocation,
1571 match_stack: vec![],
1572 root_exhausted_frame: None,
1573 is_fully_processed: false,
1574 }
1575 }
1576
1577 // metering: free for recording
1578 #[cfg(any(test, feature = "recording_mode"))]
1579 fn new_recording(function: AuthorizedFunction, current_stack_len: usize) -> Self {
1580 // Create the stack of `MatchState::Unmatched` leading to the current invocation to
1581 // represent invocations that didn't need authorization on behalf of
1582 // the tracked address.
1583 let mut match_stack = vec![MatchState::Unmatched; current_stack_len - 1];
1584 // Add a MatchState for the current(root) invocation.
1585 match_stack.push(MatchState::RootMatch);
1586 let root_exhausted_frame = Some(match_stack.len() - 1);
1587 Self {
1588 root_authorized_invocation: AuthorizedInvocation::new_recording(function),
1589 match_stack,
1590 root_exhausted_frame,
1591 is_fully_processed: false,
1592 }
1593 }
1594
1595 // Walks a path in the tree defined by `match_stack` and
1596 // returns the last visited authorized node.
1597 // metering: free
1598 fn last_authorized_invocation_mut(
1599 &mut self,
1600 ) -> Result<Option<&mut AuthorizedInvocation>, HostError> {
1601 for (i, m) in self.match_stack.iter().enumerate() {
1602 match m {
1603 MatchState::RootMatch => {
1604 return Ok(Some(
1605 self.root_authorized_invocation
1606 .last_authorized_invocation_mut(&self.match_stack, i + 1)?,
1607 ));
1608 }
1609 MatchState::SubMatch { .. } => {
1610 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into())
1611 }
1612 MatchState::Unmatched => (),
1613 }
1614 }
1615 Ok(None)
1616 }
1617
1618 // metering: covered
1619 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1620 Vec::<usize>::charge_bulk_init_cpy(1, budget)?;
1621 self.match_stack.push(MatchState::Unmatched);
1622 Ok(())
1623 }
1624
1625 // metering: free
1626 fn pop_frame(&mut self) {
1627 self.match_stack.pop();
1628 if let Some(root_exhausted_frame) = self.root_exhausted_frame {
1629 if root_exhausted_frame >= self.match_stack.len() {
1630 self.is_fully_processed = true;
1631 }
1632 }
1633 }
1634
1635 // metering: free
1636 fn is_empty(&self) -> bool {
1637 self.match_stack.is_empty()
1638 }
1639
1640 // metering: free
1641 fn is_active(&self) -> bool {
1642 self.root_authorized_invocation.is_exhausted && !self.is_fully_processed
1643 }
1644
1645 // metering: free
1646 fn current_frame_is_already_matched(&self) -> bool {
1647 match self.match_stack.last() {
1648 Some(x) => x.is_matched(),
1649 _ => false,
1650 }
1651 }
1652
1653 // Tries to match the provided invocation as an extension of the last
1654 // currently-matched sub-invocation of the authorized invocation tree (or
1655 // the root, if there is no matched invocation yet). If matching succeeds,
1656 // it writes the match to the corresponding entry in
1657 // [`InvocationTracker::match_stack`].
1658 //
1659 // Returns `true` if the match has been found for the first time per current
1660 // frame.
1661 //
1662 // Metering: covered by components
1663 fn maybe_extend_invocation_match(
1664 &mut self,
1665 host: &Host,
1666 function: &AuthorizedFunction,
1667 allow_matching_root: bool,
1668 ) -> Result<bool, HostError> {
1669 if self.current_frame_is_already_matched() {
1670 return Ok(false);
1671 }
1672 let mut new_match_state = MatchState::Unmatched;
1673 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1674 for (index_in_parent, sub_invocation) in
1675 curr_invocation.sub_invocations.iter_mut().enumerate()
1676 {
1677 if !sub_invocation.is_exhausted
1678 && host.compare(&sub_invocation.function, function)?.is_eq()
1679 {
1680 new_match_state = MatchState::SubMatch { index_in_parent };
1681 sub_invocation.is_exhausted = true;
1682 break;
1683 }
1684 }
1685 } else if !self.root_authorized_invocation.is_exhausted
1686 && allow_matching_root
1687 && host
1688 .compare(&self.root_authorized_invocation.function, &function)?
1689 .is_eq()
1690 {
1691 new_match_state = MatchState::RootMatch;
1692 self.root_authorized_invocation.is_exhausted = true;
1693 self.root_exhausted_frame = Some(self.match_stack.len() - 1);
1694 }
1695 if new_match_state.is_matched() {
1696 *self.match_stack.last_mut().ok_or_else(|| {
1697 host.err(
1698 ScErrorType::Auth,
1699 ScErrorCode::InternalError,
1700 "invalid match_stack",
1701 &[],
1702 )
1703 })? = new_match_state;
1704 }
1705 Ok(new_match_state.is_matched())
1706 }
1707
1708 // Records the invocation in this tracker.
1709 // This is needed for the recording mode only.
1710 // This assumes that the address matching is correctly performed before
1711 // calling this.
1712 // metering: free for recording
1713 #[cfg(any(test, feature = "recording_mode"))]
1714 fn record_invocation(
1715 &mut self,
1716 host: &Host,
1717 function: AuthorizedFunction,
1718 ) -> Result<(), HostError> {
1719 if self.current_frame_is_already_matched() {
1720 return Err(host.err(
1721 ScErrorType::Auth,
1722 ScErrorCode::ExistingValue,
1723 "frame is already authorized",
1724 &[],
1725 ));
1726 }
1727 // Emulate the function comparison that happens in the enforcing mode
1728 // when we're matching a `require_auth` invocation to the authorized
1729 // function from XDR.
1730 let function_for_comparison = AuthorizedFunction::from_xdr(host, function.to_xdr(host)?)?;
1731 let _ = host.compare(&function, &function_for_comparison)?;
1732 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1733 curr_invocation
1734 .sub_invocations
1735 .push(AuthorizedInvocation::new_recording(function));
1736 let index_in_parent = curr_invocation.sub_invocations.len() - 1;
1737 *self.match_stack.last_mut().unwrap() = MatchState::SubMatch { index_in_parent };
1738 } else {
1739 // This would be a bug
1740 return Err(host.err(
1741 ScErrorType::Auth,
1742 ScErrorCode::InternalError,
1743 "unexpected missing authorized invocation",
1744 &[],
1745 ));
1746 }
1747 Ok(())
1748 }
1749
1750 // metering: free
1751 #[cfg(any(test, feature = "recording_mode"))]
1752 fn has_matched_invocations_in_stack(&self) -> bool {
1753 self.match_stack.iter().any(|i| i.is_matched())
1754 }
1755
1756 // metering: covered
1757 fn snapshot(&self, budget: &Budget) -> Result<AuthorizedInvocationSnapshot, HostError> {
1758 self.root_authorized_invocation.snapshot(budget)
1759 }
1760
1761 // metering: covered
1762 fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> {
1763 self.root_authorized_invocation.rollback(snapshot)?;
1764 // Invocation can only be rolled back from 'exhausted' to
1765 // 'non-exhausted' state (as there is no other way to go from
1766 // 'exhausted' state back to 'non-exhausted' state).
1767 if !self.root_authorized_invocation.is_exhausted {
1768 self.root_exhausted_frame = None;
1769 self.is_fully_processed = false;
1770 }
1771 Ok(())
1772 }
1773}
1774
1775impl AccountAuthorizationTracker {
1776 // Metering: covered by the host and components
1777 fn from_authorization_entry(
1778 host: &Host,
1779 auth_entry: SorobanAuthorizationEntry,
1780 ) -> Result<Self, HostError> {
1781 let (address, nonce, signature, is_transaction_source_account) =
1782 match auth_entry.credentials {
1783 SorobanCredentials::SourceAccount => (
1784 host.source_account_address()?.ok_or_else(|| {
1785 host.err(
1786 ScErrorType::Auth,
1787 ScErrorCode::InternalError,
1788 "source account is missing when setting auth entries",
1789 &[],
1790 )
1791 })?,
1792 None,
1793 Val::VOID.into(),
1794 true,
1795 ),
1796 SorobanCredentials::Address(address_creds) => (
1797 host.add_host_object(address_creds.address)?,
1798 Some((
1799 address_creds.nonce,
1800 address_creds.signature_expiration_ledger,
1801 )),
1802 host.to_host_val(&address_creds.signature)?,
1803 false,
1804 ),
1805 };
1806 Ok(Self {
1807 address,
1808 invocation_tracker: InvocationTracker::from_xdr(host, auth_entry.root_invocation)?,
1809 signature,
1810 verified: false,
1811 is_transaction_source_account,
1812 nonce,
1813 })
1814 }
1815
1816 // metering: free, since this is recording mode only
1817 #[cfg(any(test, feature = "recording_mode"))]
1818 fn new_recording(
1819 host: &Host,
1820 address: AddressObject,
1821 function: AuthorizedFunction,
1822 current_stack_len: usize,
1823 ) -> Result<Self, HostError> {
1824 if current_stack_len == 0 {
1825 // This would be a bug.
1826 return Err(host.err(
1827 ScErrorType::Auth,
1828 ScErrorCode::InternalError,
1829 "unexpected empty stack in recording auth",
1830 &[],
1831 ));
1832 }
1833 // Decide if we're tracking the transaction source account, and if so
1834 // don't bother with a nonce.
1835 let is_transaction_source_account =
1836 if let Some(source_acc) = host.source_account_address()? {
1837 host.compare(&source_acc, &address)?.is_eq()
1838 } else {
1839 false
1840 };
1841 let nonce = if !is_transaction_source_account {
1842 let random_nonce: i64 =
1843 host.with_recording_auth_nonce_prng(|p| Ok(p.gen_range(0..=i64::MAX)))?;
1844 // We use the `max_live_until_ledger` as the nonce lifetime here
1845 // in order to account for a maximum possible rent fee (given maximum
1846 // possible signature expiration). However, we don't want to actually
1847 // store that as nonce expiration ledger in the recording tracker,
1848 // as users are able (and encouraged) to customize the signature
1849 // expiration after simulation and before signing the auth payload.
1850 host.consume_nonce(address, random_nonce, host.max_live_until_ledger()?)?;
1851 Some((random_nonce, 0))
1852 } else {
1853 None
1854 };
1855 Ok(Self {
1856 address,
1857 invocation_tracker: InvocationTracker::new_recording(function, current_stack_len),
1858 signature: Val::VOID.into(),
1859 verified: true,
1860 is_transaction_source_account,
1861 nonce,
1862 })
1863 }
1864
1865 // Tries to find and enforce the provided invocation with this tracker and
1866 // lazily performs authentication when needed.
1867 // This is needed for the enforcing mode only.
1868 // This assumes that the address matching is correctly performed before
1869 // calling this.
1870 // Returns true/false based on whether the invocation is found in the
1871 // tracker. Returns error if invocation has been found, but the tracker
1872 // itself is not valid (failed authentication or nonce check).
1873 // metering: covered
1874 fn maybe_authorize_invocation(
1875 &mut self,
1876 host: &Host,
1877 function: &AuthorizedFunction,
1878 allow_matching_root: bool,
1879 ) -> Result<bool, HostError> {
1880 if !self.invocation_tracker.maybe_extend_invocation_match(
1881 host,
1882 function,
1883 allow_matching_root,
1884 )? {
1885 // The call isn't found in the currently tracked tree or is already
1886 // authorized in it.
1887 // That doesn't necessarily mean it's unauthorized (it can be
1888 // authorized in a different tracker).
1889 return Ok(false);
1890 }
1891 if !self.verified {
1892 let authenticate_res = self
1893 .authenticate(host)
1894 .map_err(|err| {
1895 // Convert any recoverable errors to auth errors so that it's
1896 // not possible to confuse them for the errors of the
1897 // contract that has called `require_auth`.
1898 // While there is no 'recovery' here, non-recoverable errors
1899 // aren't really useful for decoration.
1900 if err.is_recoverable() {
1901 // Also log the original error for diagnostics.
1902 host.err(
1903 ScErrorType::Auth,
1904 ScErrorCode::InvalidAction,
1905 "failed account authentication with error",
1906 &[self.address.into(), err.error.to_val()],
1907 )
1908 } else {
1909 err
1910 }
1911 })
1912 .and_then(|_| self.verify_and_consume_nonce(host));
1913 if let Some(err) = authenticate_res.err() {
1914 return Err(err);
1915 }
1916 self.verified = true;
1917 }
1918 Ok(true)
1919 }
1920
1921 // Records the invocation in this tracker.
1922 // This is needed for the recording mode only.
1923 // This assumes that the address matching is correctly performed before
1924 // calling this.
1925 // metering: free for recording
1926 #[cfg(any(test, feature = "recording_mode"))]
1927 fn record_invocation(
1928 &mut self,
1929 host: &Host,
1930 function: AuthorizedFunction,
1931 ) -> Result<(), HostError> {
1932 self.invocation_tracker.record_invocation(host, function)
1933 }
1934
1935 // Build the authorization payload from the invocations recorded in this
1936 // tracker.
1937 // metering: free for recording
1938 #[cfg(any(test, feature = "recording_mode"))]
1939 fn get_recorded_auth_payload(&self, host: &Host) -> Result<RecordedAuthPayload, HostError> {
1940 host.as_budget().with_observable_shadow_mode(|| {
1941 Ok(RecordedAuthPayload {
1942 address: if !self.is_transaction_source_account {
1943 Some(host.visit_obj(self.address, |a: &ScAddress| a.metered_clone(host))?)
1944 } else {
1945 None
1946 },
1947 invocation: self
1948 .invocation_tracker
1949 .root_authorized_invocation
1950 .to_xdr(host, false)?,
1951 nonce: self.nonce.map(|(nonce, _)| nonce),
1952 })
1953 })
1954 }
1955
1956 // Checks if there is at least one authorized invocation in the current call
1957 // stack.
1958 // metering: free
1959 #[cfg(any(test, feature = "recording_mode"))]
1960 fn has_authorized_invocations_in_stack(&self) -> bool {
1961 self.invocation_tracker.has_matched_invocations_in_stack()
1962 }
1963
1964 // metering: covered
1965 fn root_invocation_to_xdr(
1966 &self,
1967 host: &Host,
1968 ) -> Result<xdr::SorobanAuthorizedInvocation, HostError> {
1969 self.invocation_tracker
1970 .root_authorized_invocation
1971 .to_xdr(host, false)
1972 }
1973
1974 // metering: covered
1975 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1976 self.invocation_tracker.push_frame(budget)
1977 }
1978
1979 // metering: covered
1980 fn pop_frame(&mut self) {
1981 self.invocation_tracker.pop_frame();
1982 }
1983
1984 // metering: covered
1985 fn verify_and_consume_nonce(&mut self, host: &Host) -> Result<(), HostError> {
1986 if self.is_transaction_source_account {
1987 return Ok(());
1988 }
1989 if let Some((nonce, live_until_ledger)) = &self.nonce {
1990 let ledger_seq = host.with_ledger_info(|li| Ok(li.sequence_number))?;
1991 if ledger_seq > *live_until_ledger {
1992 return Err(host.err(
1993 ScErrorType::Auth,
1994 ScErrorCode::InvalidInput,
1995 "signature has expired",
1996 &[
1997 self.address.into(),
1998 ledger_seq.try_into_val(host)?,
1999 live_until_ledger.try_into_val(host)?,
2000 ],
2001 ));
2002 }
2003 let max_live_until_ledger = host.max_live_until_ledger()?;
2004 if *live_until_ledger > max_live_until_ledger {
2005 return Err(host.err(
2006 ScErrorType::Auth,
2007 ScErrorCode::InvalidInput,
2008 "signature expiration is too late",
2009 &[
2010 self.address.into(),
2011 max_live_until_ledger.try_into_val(host)?,
2012 live_until_ledger.try_into_val(host)?,
2013 ],
2014 ));
2015 }
2016
2017 return host.consume_nonce(self.address, *nonce, *live_until_ledger);
2018 }
2019 Err(host.err(
2020 ScErrorType::Auth,
2021 ScErrorCode::InternalError,
2022 "unexpected nonce verification state",
2023 &[],
2024 ))
2025 }
2026
2027 // Computes the payload that has to be signed in order to authenticate
2028 // the authorized invocation tree corresponding to this tracker.
2029 // metering: covered by components
2030 fn get_signature_payload(&self, host: &Host) -> Result<[u8; 32], HostError> {
2031 let (nonce, live_until_ledger) = self.nonce.ok_or_else(|| {
2032 host.err(
2033 ScErrorType::Auth,
2034 ScErrorCode::InternalError,
2035 "unexpected missing nonce",
2036 &[],
2037 )
2038 })?;
2039 let payload_preimage =
2040 HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
2041 network_id: Hash(host.with_ledger_info(|li| li.network_id.metered_clone(host))?),
2042 nonce,
2043 signature_expiration_ledger: live_until_ledger,
2044 invocation: self.root_invocation_to_xdr(host)?,
2045 });
2046
2047 host.metered_hash_xdr(&payload_preimage)
2048 }
2049
2050 // metering: covered by the hsot
2051 fn authenticate(&self, host: &Host) -> Result<(), HostError> {
2052 if self.is_transaction_source_account {
2053 return Ok(());
2054 }
2055
2056 let sc_addr = host.scaddress_from_address(self.address)?;
2057 // TODO: there should also be a mode where a dummy payload is used
2058 // instead (for enforcing mode preflight).
2059 let payload = self.get_signature_payload(host)?;
2060 match sc_addr {
2061 ScAddress::Account(acc) => {
2062 check_account_authentication(host, acc, &payload, self.signature)
2063 }
2064 ScAddress::Contract(acc_contract) => check_account_contract_auth(
2065 host,
2066 &acc_contract,
2067 &payload,
2068 self.signature,
2069 &self.invocation_tracker.root_authorized_invocation,
2070 ),
2071 _ => Err(host.err(
2072 ScErrorType::Object,
2073 ScErrorCode::InternalError,
2074 "unexpected address type in auth",
2075 &[self.address.into()],
2076 )),
2077 }
2078 }
2079
2080 // Emulates authentication for the recording mode.
2081 // metering: covered
2082 #[cfg(any(test, feature = "recording_mode"))]
2083 fn emulate_authentication(&mut self, host: &Host) -> Result<(), HostError> {
2084 if self.is_transaction_source_account {
2085 return Ok(());
2086 }
2087 let sc_addr = host.scaddress_from_address(self.address)?;
2088 match sc_addr {
2089 ScAddress::Account(acc) => {
2090 // Emulate verification of a single signature that belongs to this
2091 // account.
2092 // We could emulate more (up to 20) signature verifications, but
2093 // since signature verification is a pretty expensive operation, while
2094 // multisig in combination with Soroban auth is probably pretty rare,
2095 // multisig users should either use enforcing auth simulation, or
2096 // intentionally increase the instruction count on the recording result.
2097 let key_bytes = match &acc.0 {
2098 PublicKey::PublicKeyTypeEd25519(k) => k.0,
2099 };
2100 let signature = AccountEd25519Signature {
2101 public_key: BytesN::from_slice(host, &key_bytes)?,
2102 signature: BytesN::from_slice(host, &[0_u8; 64])?,
2103 };
2104 let signatures = host_vec![host, signature]?;
2105 self.signature = signatures.into();
2106 // Authentication is expected to fail here after signature verification,
2107 // so we suppress the error and diagnostics.
2108 host.with_suppressed_diagnostic_events(|| {
2109 let _ = self.authenticate(host);
2110 Ok(())
2111 })?;
2112
2113 // Emulate a clone of the account, which serves 2 purposes:
2114 // - Account for metered clone in `get_signer_weight_from_account`
2115 // - Return budget error in case if it was suppressed above.
2116 let _ = acc.metered_clone(host.as_budget())?;
2117 }
2118 ScAddress::Contract(contract_id) => {
2119 let instance_key = host.contract_instance_ledger_key(&contract_id)?;
2120 let entry = host
2121 .try_borrow_storage_mut()?
2122 .try_get(&instance_key, host, None)?;
2123 // In test scenarios we often may not have any actual instance, which is fine most
2124 // of the time, so we don't return any errors.
2125 // In simulation scenarios the instance will likely be there, and when it's
2126 // not, we still make our best effort and include at least the necessary instance key
2127 // into the footprint.
2128 let instance = if let Some(entry) = entry {
2129 match &entry.data {
2130 LedgerEntryData::ContractData(e) => match &e.val {
2131 ScVal::ContractInstance(instance) => instance.metered_clone(host)?,
2132 _ => {
2133 return Ok(());
2134 }
2135 },
2136 _ => {
2137 return Ok(());
2138 }
2139 }
2140 } else {
2141 return Ok(());
2142 };
2143
2144 match &instance.executable {
2145 ContractExecutable::Wasm(wasm_hash) => {
2146 let wasm_key = host.contract_code_ledger_key(wasm_hash)?;
2147 let _ = host
2148 .try_borrow_storage_mut()?
2149 .try_get(&wasm_key, host, None)?;
2150 }
2151 ContractExecutable::StellarAsset => (),
2152 }
2153 }
2154 _ => {
2155 return Err(host.err(
2156 ScErrorType::Object,
2157 ScErrorCode::InternalError,
2158 "encountered unexpected ScAddress type",
2159 &[],
2160 ));
2161 }
2162 }
2163 Ok(())
2164 }
2165
2166 // metering: covered
2167 fn snapshot(&self, budget: &Budget) -> Result<AccountAuthorizationTrackerSnapshot, HostError> {
2168 Ok(AccountAuthorizationTrackerSnapshot {
2169 invocation_tracker_root_snapshot: self.invocation_tracker.snapshot(budget)?,
2170 // The verification status can be rolled back in case
2171 // of a contract failure. In case if the root call has
2172 // failed, the nonce will get 'un-consumed' due to storage
2173 // rollback. Thus we need to run verification and consume
2174 // it again in case if the call is retried. Another subtle
2175 // case where this behavior is important is the case when
2176 // custom account's authentication function depends on
2177 // some ledger state which might get modified in-between calls.
2178 verified: self.verified,
2179 })
2180 }
2181
2182 // metering: covered
2183 fn rollback(
2184 &mut self,
2185 snapshot: &AccountAuthorizationTrackerSnapshot,
2186 ) -> Result<(), HostError> {
2187 self.invocation_tracker
2188 .rollback(&snapshot.invocation_tracker_root_snapshot)?;
2189 self.verified = snapshot.verified;
2190 Ok(())
2191 }
2192
2193 // metering: free
2194 fn is_active(&self) -> bool {
2195 self.invocation_tracker.is_active()
2196 }
2197
2198 // metering: free
2199 fn current_frame_is_already_matched(&self) -> bool {
2200 self.invocation_tracker.current_frame_is_already_matched()
2201 }
2202}
2203
2204impl InvokerContractAuthorizationTracker {
2205 // metering: covered by components
2206 fn new_with_curr_contract_as_invoker(
2207 host: &Host,
2208 invoker_auth_entry: Val,
2209 ) -> Result<Self, HostError> {
2210 let invoker_sc_addr = ScAddress::Contract(host.get_current_contract_id_internal()?);
2211 let authorized_invocation = invoker_contract_auth_to_authorized_invocation(
2212 host,
2213 &invoker_sc_addr,
2214 invoker_auth_entry,
2215 )?;
2216 let invocation_tracker = InvocationTracker::new(authorized_invocation);
2217 Ok(Self {
2218 contract_address: host.add_host_object(invoker_sc_addr)?,
2219 invocation_tracker,
2220 })
2221 }
2222
2223 // metering: covered
2224 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
2225 self.invocation_tracker.push_frame(budget)
2226 }
2227
2228 // metering: covered
2229 fn pop_frame(&mut self) {
2230 self.invocation_tracker.pop_frame();
2231 }
2232
2233 // metering: free
2234 fn is_out_of_scope(&self) -> bool {
2235 self.invocation_tracker.is_empty()
2236 }
2237
2238 // metering: covered
2239 fn maybe_authorize_invocation(
2240 &mut self,
2241 host: &Host,
2242 function: &AuthorizedFunction,
2243 ) -> Result<bool, HostError> {
2244 // Authorization is successful if function is just matched by the
2245 // tracker. No authentication is needed.
2246 self.invocation_tracker
2247 .maybe_extend_invocation_match(host, function, true)
2248 }
2249}
2250
2251impl Host {
2252 // metering: covered by components
2253 fn consume_nonce(
2254 &self,
2255 address: AddressObject,
2256 nonce: i64,
2257 live_until_ledger: u32,
2258 ) -> Result<(), HostError> {
2259 let nonce_key_scval = ScVal::LedgerKeyNonce(ScNonceKey { nonce });
2260 let sc_address = self.scaddress_from_address(address)?;
2261 let nonce_key = self.storage_key_for_address(
2262 sc_address.metered_clone(self)?,
2263 nonce_key_scval.metered_clone(self)?,
2264 xdr::ContractDataDurability::Temporary,
2265 )?;
2266 let live_until_ledger = live_until_ledger
2267 .max(self.get_min_live_until_ledger(xdr::ContractDataDurability::Temporary)?);
2268 self.with_mut_storage(|storage| {
2269 if storage.has(&nonce_key, self, None).map_err(|err| {
2270 if err.error.is_type(ScErrorType::Storage)
2271 && err.error.is_code(ScErrorCode::ExceededLimit)
2272 {
2273 return self.err(
2274 ScErrorType::Storage,
2275 ScErrorCode::ExceededLimit,
2276 "trying to access nonce outside of footprint for address",
2277 &[address.to_val()],
2278 );
2279 }
2280 err
2281 })? {
2282 return Err(self.err(
2283 ScErrorType::Auth,
2284 ScErrorCode::ExistingValue,
2285 "nonce already exists for address",
2286 &[address.into()],
2287 ));
2288 }
2289 let data = LedgerEntryData::ContractData(ContractDataEntry {
2290 contract: sc_address,
2291 key: nonce_key_scval,
2292 val: ScVal::Void,
2293 durability: xdr::ContractDataDurability::Temporary,
2294 ext: xdr::ExtensionPoint::V0,
2295 });
2296 let entry = LedgerEntry {
2297 last_modified_ledger_seq: 0,
2298 data,
2299 ext: LedgerEntryExt::V0,
2300 };
2301 storage.put(
2302 &nonce_key,
2303 &Rc::metered_new(entry, self)?,
2304 Some(live_until_ledger),
2305 self,
2306 None,
2307 )
2308 })
2309 }
2310
2311 // Returns the recorded per-address authorization payloads that would cover the
2312 // top-level contract function invocation in the enforcing mode.
2313 // This should only be called in the recording authorization mode, i.e. only
2314 // if `switch_to_recording_auth` has been called.
2315 #[cfg(any(test, feature = "recording_mode"))]
2316 pub fn get_recorded_auth_payloads(&self) -> Result<Vec<RecordedAuthPayload>, HostError> {
2317 #[cfg(not(any(test, feature = "testutils")))]
2318 {
2319 self.try_borrow_authorization_manager()?
2320 .get_recorded_auth_payloads(self)
2321 }
2322 #[cfg(any(test, feature = "testutils"))]
2323 {
2324 let payloads = self
2325 .try_borrow_previous_authorization_manager()?
2326 .as_ref()
2327 .ok_or_else(|| {
2328 self.err(
2329 ScErrorType::Auth,
2330 ScErrorCode::InvalidAction,
2331 "previous invocation is missing - no auth data to get",
2332 &[],
2333 )
2334 })?
2335 .get_recorded_auth_payloads(self)?;
2336 Ok(payloads)
2337 }
2338 }
2339}
2340
2341#[cfg(any(test, feature = "testutils"))]
2342use crate::{host::frame::CallParams, xdr::SorobanAuthorizedInvocation};
2343
2344#[cfg(any(test, feature = "testutils"))]
2345impl Host {
2346 /// Invokes the reserved `__check_auth` function on a provided contract.
2347 ///
2348 /// This is useful for testing the custom account contracts. Otherwise, the
2349 /// host prohibits calling `__check_auth` outside of internal implementation
2350 /// of `require_auth[_for_args]` calls.
2351 pub fn call_account_contract_check_auth(
2352 &self,
2353 contract: AddressObject,
2354 args: VecObject,
2355 ) -> Result<Val, HostError> {
2356 let _invocation_meter_scope = self.maybe_meter_invocation(
2357 crate::host::invocation_metering::MeteringInvocation::check_auth_invocation(
2358 self, contract,
2359 ),
2360 );
2361
2362 use crate::builtin_contracts::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME;
2363 let contract_id = self.contract_id_from_address(contract)?;
2364 let args_vec = self.call_args_from_obj(args)?;
2365 let res = self.call_n_internal(
2366 &contract_id,
2367 ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(self)?,
2368 args_vec.as_slice(),
2369 CallParams::default_internal_call(),
2370 );
2371 if let Err(e) = &res {
2372 use crate::ErrorHandler;
2373 self.error(
2374 e.error,
2375 "check auth invocation for a custom account contract failed",
2376 &[contract.to_val(), args.to_val()],
2377 );
2378 }
2379 res
2380 }
2381
2382 /// Returns the current state of the authorization manager.
2383 ///
2384 /// Use this in conjunction with `set_auth_manager` to do authorized
2385 /// operations without breaking the current authorization state (useful for
2386 /// preserving the auth state while doing the generic test setup).
2387 pub fn snapshot_auth_manager(&self) -> Result<AuthorizationManager, HostError> {
2388 Ok(self.try_borrow_authorization_manager()?.clone())
2389 }
2390
2391 /// Switches host to the recording authorization mode and inherits the
2392 /// recording mode settings from the provided authorization manager settings
2393 /// in case if it used the recording mode.
2394 ///
2395 /// This is similar to `switch_to_recording_auth`, but should be preferred
2396 /// to use in conjunction with `snapshot_auth_manager`, such that the
2397 /// recording mode settings are not overridden.
2398 pub fn switch_to_recording_auth_inherited_from_snapshot(
2399 &self,
2400 auth_manager_snapshot: &AuthorizationManager,
2401 ) -> Result<(), HostError> {
2402 let disable_non_root_auth = match &auth_manager_snapshot.mode {
2403 AuthorizationMode::Enforcing => true,
2404 AuthorizationMode::Recording(recording_auth_info) => {
2405 recording_auth_info.disable_non_root_auth
2406 }
2407 };
2408 *self.try_borrow_authorization_manager_mut()? =
2409 AuthorizationManager::new_recording(disable_non_root_auth);
2410 Ok(())
2411 }
2412
2413 /// Replaces authorization manager with the provided new instance.
2414 ///
2415 /// Use this in conjunction with `snapshot_auth_manager` to do authorized
2416 /// operations without breaking the current authorization state (useful for
2417 /// preserving the auth state while doing the generic test setup).
2418 pub fn set_auth_manager(&self, auth_manager: AuthorizationManager) -> Result<(), HostError> {
2419 *self.try_borrow_authorization_manager_mut()? = auth_manager;
2420 Ok(())
2421 }
2422
2423 // Returns the authorizations that have been authenticated for the last
2424 // contract invocation.
2425 //
2426 // Authenticated means that either the authorization was authenticated using
2427 // the actual authorization logic for that authorization in enforced mode,
2428 // or that it was recorded in recording mode and authorization was assumed
2429 // successful.
2430 pub fn get_authenticated_authorizations(
2431 &self,
2432 ) -> Result<Vec<(ScAddress, SorobanAuthorizedInvocation)>, HostError> {
2433 Ok(self
2434 .try_borrow_previous_authorization_manager_mut()?
2435 .as_mut()
2436 .map(|am| am.get_authenticated_authorizations(self))
2437 // If no AuthorizationManager is setup, no authorizations could have
2438 // taken place so return an empty vec.
2439 .unwrap_or_default())
2440 }
2441}
2442
2443// metering: free for testutils
2444#[cfg(any(test, feature = "testutils"))]
2445impl PartialEq for RecordedAuthPayload {
2446 fn eq(&self, other: &Self) -> bool {
2447 self.address == other.address
2448 && self.invocation == other.invocation
2449 // Compare nonces only by presence of the value - recording mode
2450 // generates random nonces.
2451 && self.nonce.is_some() == other.nonce.is_some()
2452 }
2453}