soroban_env_host_zephyr/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, CreateContractArgs, HashIdPreimage, HashIdPreimageSorobanAuthorization,
165 InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, ScAddress, ScErrorCode,
166 ScErrorType, ScNonceKey, ScVal, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
167 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::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(CreateContractArgs),
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(CreateContractArgs),
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(xdr_args)
607 }
608 })
609 }
610
611 // metering: covered by the host
612 fn to_xdr(&self, host: &Host) -> Result<SorobanAuthorizedFunction, HostError> {
613 match self {
614 AuthorizedFunction::ContractFn(contract_fn) => {
615 let function_name = host.scsymbol_from_symbol(contract_fn.function_name)?;
616 Ok(SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
617 contract_address: host.scaddress_from_address(contract_fn.contract_address)?,
618 function_name,
619 args: host.vals_to_scval_vec(contract_fn.args.as_slice())?,
620 }))
621 }
622 AuthorizedFunction::CreateContractHostFn(create_contract_args) => {
623 Ok(SorobanAuthorizedFunction::CreateContractHostFn(
624 create_contract_args.metered_clone(host)?,
625 ))
626 }
627 }
628 }
629}
630
631impl AuthorizedInvocation {
632 // metering: covered
633 fn from_xdr(
634 host: &Host,
635 xdr_invocation: xdr::SorobanAuthorizedInvocation,
636 ) -> Result<Self, HostError> {
637 let sub_invocations_xdr = xdr_invocation.sub_invocations.into_vec();
638 let sub_invocations = sub_invocations_xdr
639 .into_iter()
640 .map(|a| AuthorizedInvocation::from_xdr(host, a))
641 .metered_collect::<Result<Vec<_>, _>>(host)??;
642 Ok(Self {
643 function: AuthorizedFunction::from_xdr(host, xdr_invocation.function)?,
644 sub_invocations,
645 is_exhausted: false,
646 })
647 }
648
649 // metering: free
650 pub(crate) fn new(
651 function: AuthorizedFunction,
652 sub_invocations: Vec<AuthorizedInvocation>,
653 ) -> Self {
654 Self {
655 function,
656 sub_invocations,
657 is_exhausted: false,
658 }
659 }
660
661 // metering: free
662 #[cfg(any(test, feature = "recording_mode"))]
663 fn new_recording(function: AuthorizedFunction) -> Self {
664 Self {
665 function,
666 sub_invocations: vec![],
667 is_exhausted: true,
668 }
669 }
670
671 // metering: covered
672 fn to_xdr(
673 &self,
674 host: &Host,
675 exhausted_sub_invocations_only: bool,
676 ) -> Result<xdr::SorobanAuthorizedInvocation, HostError> {
677 Ok(xdr::SorobanAuthorizedInvocation {
678 function: self.function.to_xdr(host)?,
679 sub_invocations: self
680 .sub_invocations
681 .iter()
682 .filter(|i| i.is_exhausted || !exhausted_sub_invocations_only)
683 .map(|i| i.to_xdr(host, exhausted_sub_invocations_only))
684 .metered_collect::<Result<Vec<xdr::SorobanAuthorizedInvocation>, HostError>>(host)??
685 .try_into()?,
686 })
687 }
688
689 // Walks a path in the tree defined by `match_stack` and
690 // returns the last visited authorized node.
691 // metering: free
692 fn last_authorized_invocation_mut(
693 &mut self,
694 match_stack: &Vec<MatchState>,
695 call_stack_id: usize,
696 ) -> Result<&mut AuthorizedInvocation, HostError> {
697 // Start walking the stack from `call_stack_id`. We trust the callers to
698 // hold the invariant that `match_stack[call_stack_id - 1]`
699 // corresponds to this invocation tree, so that the next non-`None` child
700 // corresponds to the child of the current tree.
701 for (i, m) in match_stack.iter().enumerate().skip(call_stack_id) {
702 match m {
703 MatchState::SubMatch { index_in_parent } => {
704 // We trust the caller to have the correct sub-invocation
705 // indices.
706 if let Some(sub) = self.sub_invocations.get_mut(*index_in_parent) {
707 return sub.last_authorized_invocation_mut(match_stack, i + 1);
708 } else {
709 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
710 }
711 }
712 MatchState::RootMatch => {
713 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
714 }
715 // Skip Unmatched invocations as they don't require authorization.
716 MatchState::Unmatched => (),
717 }
718 }
719 Ok(self)
720 }
721
722 // metering: covered
723 fn snapshot(&self, budget: &Budget) -> Result<AuthorizedInvocationSnapshot, HostError> {
724 Ok(AuthorizedInvocationSnapshot {
725 is_exhausted: self.is_exhausted,
726 sub_invocations: self
727 .sub_invocations
728 .iter()
729 .map(|i| i.snapshot(budget))
730 .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(
731 budget,
732 )??,
733 })
734 }
735
736 // metering: free
737 fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> {
738 self.is_exhausted = snapshot.is_exhausted;
739 if self.sub_invocations.len() != snapshot.sub_invocations.len() {
740 // This would be a bug.
741 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
742 }
743 for (sub, snap) in self
744 .sub_invocations
745 .iter_mut()
746 .zip(snapshot.sub_invocations.iter())
747 {
748 sub.rollback(snap)?
749 }
750 Ok(())
751 }
752}
753
754impl Default for AuthorizationManager {
755 fn default() -> Self {
756 Self::new_enforcing_without_authorizations()
757 }
758}
759
760impl AuthorizationManager {
761 // Creates a new enforcing `AuthorizationManager` from the given
762 // authorization entries.
763 // This should be created once per top-level invocation.
764 // metering: covered
765 pub(crate) fn new_enforcing(
766 host: &Host,
767 auth_entries: Vec<SorobanAuthorizationEntry>,
768 ) -> Result<Self, HostError> {
769 let mut trackers = Vec::<RefCell<AccountAuthorizationTracker>>::with_metered_capacity(
770 auth_entries.len(),
771 host,
772 )?;
773 for auth_entry in auth_entries {
774 trackers.push(RefCell::new(
775 AccountAuthorizationTracker::from_authorization_entry(host, auth_entry)?,
776 ));
777 }
778 Ok(Self {
779 mode: AuthorizationMode::Enforcing,
780 call_stack: RefCell::new(vec![]),
781 account_trackers: RefCell::new(trackers),
782 invoker_contract_trackers: RefCell::new(vec![]),
783 })
784 }
785
786 // Creates a new enforcing `AuthorizationManager` that doesn't allow any
787 // authorizations.
788 // This is useful as a safe default mode.
789 // metering: free
790 pub(crate) fn new_enforcing_without_authorizations() -> Self {
791 Self {
792 mode: AuthorizationMode::Enforcing,
793 call_stack: RefCell::new(vec![]),
794 account_trackers: RefCell::new(vec![]),
795 invoker_contract_trackers: RefCell::new(vec![]),
796 }
797 }
798
799 // Creates a new recording `AuthorizationManager`.
800 // All the authorization requirements will be recorded and can then be
801 // retrieved using `get_recorded_auth_payloads`.
802 // metering: free
803 #[cfg(any(test, feature = "recording_mode"))]
804 pub(crate) fn new_recording(disable_non_root_auth: bool) -> Self {
805 Self {
806 mode: AuthorizationMode::Recording(RecordingAuthInfo {
807 tracker_by_address_handle: Default::default(),
808 disable_non_root_auth,
809 }),
810 call_stack: RefCell::new(vec![]),
811 account_trackers: RefCell::new(vec![]),
812 invoker_contract_trackers: RefCell::new(vec![]),
813 }
814 }
815
816 // Require the `address` to have authorized the current contract invocation
817 // with provided args and within the current context (i.e. the current
818 // authorized call stack and for the current network).
819 // In the recording mode this stores the auth requirement instead of
820 // verifying it.
821 // metering: covered
822 pub(crate) fn require_auth(
823 &self,
824 host: &Host,
825 address: AddressObject,
826 args: Vec<Val>,
827 ) -> Result<(), HostError> {
828 let _span = tracy_span!("require auth");
829 let authorized_function = self
830 .try_borrow_call_stack(host)?
831 .last()
832 .ok_or_else(|| {
833 host.err(
834 ScErrorType::Auth,
835 ScErrorCode::InternalError,
836 "unexpected require_auth outside of valid frame",
837 &[],
838 )
839 })?
840 .to_authorized_function(host, args)?;
841
842 self.require_auth_internal(host, address, authorized_function)
843 }
844
845 // metering: covered
846 pub(crate) fn add_invoker_contract_auth_with_curr_contract_as_invoker(
847 &self,
848 host: &Host,
849 auth_entries: VecObject,
850 ) -> Result<(), HostError> {
851 let auth_entries =
852 host.visit_obj(auth_entries, |e: &HostVec| e.to_vec(host.budget_ref()))?;
853 let mut trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
854 Vec::<InvokerContractAuthorizationTracker>::charge_bulk_init_cpy(
855 auth_entries.len() as u64,
856 host,
857 )?;
858 trackers.reserve(auth_entries.len());
859 for e in auth_entries {
860 trackers.push(
861 InvokerContractAuthorizationTracker::new_with_curr_contract_as_invoker(host, e)?,
862 )
863 }
864 Ok(())
865 }
866
867 // metering: covered by components
868 fn maybe_check_invoker_contract_auth(
869 &self,
870 host: &Host,
871 address: AddressObject,
872 function: &AuthorizedFunction,
873 ) -> Result<bool, HostError> {
874 {
875 let call_stack = self.try_borrow_call_stack(host)?;
876 // If stack has just one call there can't be invoker.
877 if call_stack.len() < 2 {
878 return Ok(false);
879 }
880
881 // Try matching the direct invoker contract first. It is considered to
882 // have authorized any direct calls.
883 let Some(invoker_frame) = &call_stack.get(call_stack.len() - 2) else {
884 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into());
885 };
886 if let AuthStackFrame::Contract(invoker_contract) = invoker_frame {
887 if host
888 .compare(&invoker_contract.contract_address, &address)?
889 .is_eq()
890 {
891 return Ok(true);
892 }
893 }
894 }
895 let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
896 // If there is no direct invoker, there still might be a valid
897 // sub-contract call authorization from another invoker higher up the
898 // stack. Note, that invoker contract trackers consider the direct frame
899 // to never require auth (any `require_auth` calls would be matched by
900 // logic above).
901 for tracker in invoker_contract_trackers.iter_mut() {
902 if host.compare(&tracker.contract_address, &address)?.is_eq()
903 && tracker.maybe_authorize_invocation(host, function)?
904 {
905 return Ok(true);
906 }
907 }
908
909 return Ok(false);
910 }
911
912 // metering: covered by components
913 fn require_auth_enforcing(
914 &self,
915 host: &Host,
916 address: AddressObject,
917 function: &AuthorizedFunction,
918 ) -> Result<(), HostError> {
919 // Find if there is already an active tracker for this address that has
920 // not been matched for the current frame. If there is such tracker,
921 // this authorization has to be matched with an already active tracker.
922 // This prevents matching sets of disjoint authorization entries to
923 // a tree of calls.
924 let mut has_active_tracker = false;
925 for tracker in self.try_borrow_account_trackers(host)?.iter() {
926 if let Ok(tracker) = tracker.try_borrow() {
927 // If address doesn't match, just skip the tracker.
928 if host.compare(&tracker.address, &address)?.is_eq()
929 && tracker.is_active()
930 && !tracker.current_frame_is_already_matched()
931 {
932 has_active_tracker = true;
933 break;
934 }
935 }
936 }
937
938 // Iterate all the trackers and try to find one that
939 // fulfills the authorization requirement.
940 for tracker in self.try_borrow_account_trackers(host)?.iter() {
941 // Tracker can only be borrowed by the authorization manager itself.
942 // The only scenario in which re-borrow might occur is when
943 // `require_auth` is called within `__check_auth` call. The tracker
944 // that called `__check_auth` would be already borrowed in such
945 // scenario.
946 // We allow such call patterns in general, but we don't allow using
947 // tracker to verify auth for itself, i.e. we don't allow something
948 // like address.require_auth()->address_contract.__check_auth()
949 // ->address.require_auth(). Thus we simply skip the trackers that
950 // have already been borrowed.
951 if let Ok(mut tracker) = tracker.try_borrow_mut() {
952 // If tracker has already been used for this frame or the address
953 // doesn't match, just skip the tracker.
954 if !host.compare(&tracker.address, &address)?.is_eq() {
955 continue;
956 }
957 match tracker.maybe_authorize_invocation(host, function, !has_active_tracker) {
958 // If tracker doesn't have a matching invocation,
959 // just skip it (there could still be another
960 // tracker that matches it).
961 Ok(false) => continue,
962 // Found a matching authorization.
963 Ok(true) => return Ok(()),
964 // Found a matching authorization, but another
965 // requirement hasn't been fulfilled (for
966 // example, incorrect authentication or nonce).
967 Err(e) => return Err(e),
968 }
969 }
970 }
971 // No matching tracker found, hence the invocation isn't
972 // authorized.
973 Err(host.err(
974 ScErrorType::Auth,
975 ScErrorCode::InvalidAction,
976 "Unauthorized function call for address",
977 &[address.to_val()],
978 ))
979 }
980
981 #[cfg(any(test, feature = "recording_mode"))]
982 fn require_auth_recording(
983 &self,
984 host: &Host,
985 address: AddressObject,
986 function: AuthorizedFunction,
987 recording_info: &RecordingAuthInfo,
988 ) -> Result<(), HostError> {
989 // At first, try to find the tracker for this exact address
990 // object.
991 // This is a best-effort heuristic to come up with a reasonably
992 // looking recording tree for cases when multiple instances of
993 // the same exact address are used.
994 let address_obj_handle = address.get_handle();
995 let existing_tracker_id = recording_info
996 .try_borrow_tracker_by_address_handle(host)?
997 .get(&address_obj_handle)
998 .copied();
999 if let Some(tracker_id) = existing_tracker_id {
1000 // The tracker should not be borrowed recursively in
1001 // recording mode, as we don't call `__check_auth` in this
1002 // flow.
1003 let trackers = self.try_borrow_account_trackers(host)?;
1004 let Some(trackercell) = trackers.get(tracker_id) else {
1005 return Err(host.err(
1006 ScErrorType::Auth,
1007 ScErrorCode::InternalError,
1008 "bad index for existing tracker",
1009 &[],
1010 ));
1011 };
1012 if let Ok(mut tracker) = trackercell.try_borrow_mut() {
1013 // The recording invariant is that trackers are created
1014 // with the first authorized invocation, which means
1015 // that when their stack no longer has authorized
1016 // invocation, then we've popped frames past its root
1017 // and hence need to create a new tracker.
1018 if !tracker.has_authorized_invocations_in_stack() {
1019 recording_info
1020 .try_borrow_tracker_by_address_handle_mut(host)?
1021 .remove(&address_obj_handle);
1022 } else {
1023 return tracker.record_invocation(host, function);
1024 }
1025 } else {
1026 return Err(host.err(
1027 ScErrorType::Auth,
1028 ScErrorCode::InternalError,
1029 "unexpected recursive tracker borrow in recording mode",
1030 &[],
1031 ));
1032 };
1033 }
1034 // If there is no active tracker for this exact address object,
1035 // try to find any matching active tracker for the address.
1036 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1037 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1038 if !host.compare(&tracker.address, &address)?.is_eq() {
1039 continue;
1040 }
1041 // Take the first tracker that is still active (i.e. has
1042 // active authorizations in the current call stack) and
1043 // hasn't been used for this stack frame yet.
1044 if tracker.has_authorized_invocations_in_stack()
1045 && !tracker.current_frame_is_already_matched()
1046 {
1047 return tracker.record_invocation(host, function);
1048 }
1049 } else {
1050 return Err(host.err(
1051 ScErrorType::Auth,
1052 ScErrorCode::InternalError,
1053 "unexpected borrowed tracker in recording auth mode",
1054 &[],
1055 ));
1056 }
1057 }
1058 // At this stage there is no active tracker to which we could
1059 // match the current invocation, thus we need to create a new
1060 // tracker.
1061 // Alert the user in `disable_non_root_auth` mode if we're not
1062 // in the root stack frame.
1063 if recording_info.disable_non_root_auth && self.try_borrow_call_stack(host)?.len() != 1 {
1064 return Err(host.err(
1065 ScErrorType::Auth,
1066 ScErrorCode::InvalidAction,
1067 "[recording authorization only] encountered authorization not tied \
1068 to the root contract invocation for an address. Use `require_auth()` \
1069 in the top invocation or enable non-root authorization.",
1070 &[address.into()],
1071 ));
1072 }
1073 // If a tracker for the new tree doesn't exist yet, create
1074 // it and initialize with the current invocation.
1075 self.try_borrow_account_trackers_mut(host)?
1076 .push(RefCell::new(AccountAuthorizationTracker::new_recording(
1077 host,
1078 address,
1079 function,
1080 self.try_borrow_call_stack(host)?.len(),
1081 )?));
1082 recording_info
1083 .try_borrow_tracker_by_address_handle_mut(host)?
1084 .insert(
1085 address_obj_handle,
1086 self.try_borrow_account_trackers(host)?.len() - 1,
1087 );
1088 Ok(())
1089 }
1090
1091 // metering: covered
1092 fn require_auth_internal(
1093 &self,
1094 host: &Host,
1095 address: AddressObject,
1096 function: AuthorizedFunction,
1097 ) -> Result<(), HostError> {
1098 // First check the InvokerContractAuthorizationTrackers
1099 if self.maybe_check_invoker_contract_auth(host, address, &function)? {
1100 return Ok(());
1101 }
1102 // Then check the AccountAuthorizationTrackers
1103 match &self.mode {
1104 AuthorizationMode::Enforcing => self.require_auth_enforcing(host, address, &function),
1105 // metering: free for recording
1106 #[cfg(any(test, feature = "recording_mode"))]
1107 AuthorizationMode::Recording(recording_info) => {
1108 self.require_auth_recording(host, address, function, recording_info)
1109 }
1110 }
1111 }
1112
1113 // Returns a snapshot of `AuthorizationManager` to use for rollback.
1114 // metering: covered
1115 fn snapshot(&self, host: &Host) -> Result<AuthorizationManagerSnapshot, HostError> {
1116 let _span = tracy_span!("snapshot auth");
1117 let account_trackers_snapshot = match &self.mode {
1118 AuthorizationMode::Enforcing => {
1119 let len = self.try_borrow_account_trackers(host)?.len();
1120 let mut snapshots =
1121 Vec::<Option<AccountAuthorizationTrackerSnapshot>>::with_metered_capacity(
1122 len, host,
1123 )?;
1124 for t in self.try_borrow_account_trackers(host)?.iter() {
1125 let sp = if let Ok(tracker) = t.try_borrow() {
1126 Some(tracker.snapshot(host.as_budget())?)
1127 } else {
1128 // If tracker is borrowed, snapshotting it is a no-op
1129 // (it can't change until we release it higher up the
1130 // stack).
1131 None
1132 };
1133 snapshots.push(sp);
1134 }
1135 AccountTrackersSnapshot::Enforcing(snapshots)
1136 }
1137 #[cfg(any(test, feature = "recording_mode"))]
1138 AuthorizationMode::Recording(_) => {
1139 // All trackers should be available to borrow for copy as in
1140 // recording mode we can't have recursive authorization.
1141 // metering: free for recording
1142 AccountTrackersSnapshot::Recording(self.try_borrow_account_trackers(host)?.clone())
1143 }
1144 };
1145 let invoker_contract_tracker_root_snapshots = self
1146 .try_borrow_invoker_contract_trackers(host)?
1147 .iter()
1148 .map(|t| t.invocation_tracker.snapshot(host.as_budget()))
1149 .metered_collect::<Result<Vec<AuthorizedInvocationSnapshot>, HostError>>(host)??;
1150 #[cfg(any(test, feature = "recording_mode"))]
1151 let tracker_by_address_handle = match &self.mode {
1152 AuthorizationMode::Enforcing => None,
1153 AuthorizationMode::Recording(recording_info) => Some(
1154 // metering: free for recording
1155 recording_info
1156 .try_borrow_tracker_by_address_handle(host)?
1157 .clone(),
1158 ),
1159 };
1160 Ok(AuthorizationManagerSnapshot {
1161 account_trackers_snapshot,
1162 invoker_contract_tracker_root_snapshots,
1163 #[cfg(any(test, feature = "recording_mode"))]
1164 tracker_by_address_handle,
1165 })
1166 }
1167
1168 // Rolls back this `AuthorizationManager` to the snapshot state.
1169 // metering: covered
1170 fn rollback(
1171 &self,
1172 host: &Host,
1173 snapshot: AuthorizationManagerSnapshot,
1174 ) -> Result<(), HostError> {
1175 let _span = tracy_span!("rollback auth");
1176 match snapshot.account_trackers_snapshot {
1177 AccountTrackersSnapshot::Enforcing(trackers_snapshot) => {
1178 let trackers = self.try_borrow_account_trackers(host)?;
1179 if trackers.len() != trackers_snapshot.len() {
1180 return Err(host.err(
1181 ScErrorType::Auth,
1182 ScErrorCode::InternalError,
1183 "unexpected bad auth snapshot",
1184 &[],
1185 ));
1186 }
1187 for (i, tracker) in trackers.iter().enumerate() {
1188 let Some(snapopt) = trackers_snapshot.get(i) else {
1189 return Err(host.err(
1190 ScErrorType::Auth,
1191 ScErrorCode::InternalError,
1192 "unexpected auth snapshot index",
1193 &[],
1194 ));
1195 };
1196 if let Some(tracker_snapshot) = snapopt {
1197 tracker
1198 .try_borrow_mut()
1199 .map_err(|_| {
1200 host.err(
1201 ScErrorType::Auth,
1202 ScErrorCode::InternalError,
1203 "unexpected bad auth borrow",
1204 &[],
1205 )
1206 })?
1207 .rollback(&tracker_snapshot)?;
1208 }
1209 }
1210 }
1211 #[cfg(any(test, feature = "recording_mode"))]
1212 AccountTrackersSnapshot::Recording(s) => {
1213 *self.try_borrow_account_trackers_mut(host)? = s;
1214 }
1215 }
1216
1217 let mut invoker_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1218 if invoker_trackers.len() != snapshot.invoker_contract_tracker_root_snapshots.len() {
1219 return Err(host.err(
1220 ScErrorType::Auth,
1221 ScErrorCode::InternalError,
1222 "unexpected bad auth snapshot",
1223 &[],
1224 ));
1225 }
1226 for (tracker, snapshot) in invoker_trackers
1227 .iter_mut()
1228 .zip(snapshot.invoker_contract_tracker_root_snapshots.iter())
1229 {
1230 tracker.invocation_tracker.rollback(snapshot)?;
1231 }
1232
1233 #[cfg(any(test, feature = "recording_mode"))]
1234 if let Some(tracker_by_address_handle) = snapshot.tracker_by_address_handle {
1235 match &self.mode {
1236 AuthorizationMode::Enforcing => (),
1237 AuthorizationMode::Recording(recording_info) => {
1238 *recording_info.try_borrow_tracker_by_address_handle_mut(host)? =
1239 tracker_by_address_handle;
1240 }
1241 }
1242 }
1243 Ok(())
1244 }
1245
1246 // metering: covered
1247 fn push_tracker_frame(&self, host: &Host) -> Result<(), HostError> {
1248 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1249 // Skip already borrowed trackers, these must be in the middle of
1250 // authentication and hence don't need stack to be updated.
1251 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1252 tracker.push_frame(host.as_budget())?;
1253 }
1254 }
1255 for tracker in self
1256 .try_borrow_invoker_contract_trackers_mut(host)?
1257 .iter_mut()
1258 {
1259 tracker.push_frame(host.as_budget())?;
1260 }
1261 Ok(())
1262 }
1263
1264 // metering: covered
1265 pub(crate) fn push_create_contract_host_fn_frame(
1266 &self,
1267 host: &Host,
1268 args: CreateContractArgs,
1269 ) -> Result<(), HostError> {
1270 Vec::<CreateContractArgs>::charge_bulk_init_cpy(1, host)?;
1271 self.try_borrow_call_stack_mut(host)?
1272 .push(AuthStackFrame::CreateContractHostFn(args));
1273 self.push_tracker_frame(host)
1274 }
1275
1276 // Records a new call stack frame and returns a snapshot for rolling
1277 // back this stack frame.
1278 // This should be called for every `Host` `push_frame`.
1279 // metering: covered
1280 pub(crate) fn push_frame(
1281 &self,
1282 host: &Host,
1283 frame: &Frame,
1284 ) -> Result<AuthorizationManagerSnapshot, HostError> {
1285 let _span = tracy_span!("push auth frame");
1286 let (contract_id, function_name) = match frame {
1287 Frame::ContractVM { vm, fn_name, .. } => {
1288 (vm.contract_id.metered_clone(host)?, *fn_name)
1289 }
1290 // Skip the top-level host function stack frames as they don't
1291 // contain all the necessary information.
1292 // Use the respective push (like
1293 // `push_create_contract_host_fn_frame`) functions instead to push
1294 // the frame with the required info.
1295 Frame::HostFunction(_) => return self.snapshot(host),
1296 Frame::StellarAssetContract(id, fn_name, ..) => (id.metered_clone(host)?, *fn_name),
1297 #[cfg(any(test, feature = "testutils"))]
1298 Frame::TestContract(tc) => (tc.id.metered_clone(host)?, tc.func),
1299 };
1300 let contract_address = host.add_host_object(ScAddress::Contract(contract_id))?;
1301 Vec::<ContractInvocation>::charge_bulk_init_cpy(1, host)?;
1302 self.try_borrow_call_stack_mut(host)?
1303 .push(AuthStackFrame::Contract(ContractInvocation {
1304 contract_address,
1305 function_name,
1306 }));
1307
1308 self.push_tracker_frame(host)?;
1309 self.snapshot(host)
1310 }
1311
1312 // Pops a call stack frame and maybe rolls back the internal
1313 // state according to the provided snapshot.
1314 // This should be called for every `Host` `pop_frame`.
1315 // metering: covered
1316 pub(crate) fn pop_frame(
1317 &self,
1318 host: &Host,
1319 snapshot: Option<AuthorizationManagerSnapshot>,
1320 ) -> Result<(), HostError> {
1321 let _span = tracy_span!("pop auth frame");
1322 // Important: rollback has to be performed before popping the frame
1323 // from the tracker. This ensures correct work of invoker contract
1324 // trackers for which snapshots are dependent on the current
1325 // call stack.
1326 if let Some(snapshot) = snapshot {
1327 self.rollback(host, snapshot)?;
1328 }
1329 {
1330 let mut call_stack = self.try_borrow_call_stack_mut(host)?;
1331 // Currently we don't push host function call frames, hence this may be
1332 // called with empty stack. We trust the Host to keep things correct,
1333 // i.e. that only host function frames are ignored this way.
1334 if call_stack.is_empty() {
1335 return Ok(());
1336 }
1337 call_stack.pop();
1338 }
1339 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1340 // Skip already borrowed trackers, these must be in the middle of
1341 // authentication and hence don't need stack to be updated.
1342 if let Ok(mut tracker) = tracker.try_borrow_mut() {
1343 tracker.pop_frame();
1344 }
1345 }
1346
1347 let mut invoker_contract_trackers = self.try_borrow_invoker_contract_trackers_mut(host)?;
1348 for tracker in invoker_contract_trackers.iter_mut() {
1349 tracker.pop_frame();
1350 }
1351 // Pop invoker contract trackers that went out of scope. The invariant
1352 // is that tracker only exists for the next sub-contract call (or until
1353 // the tracker's frame itself is popped). Thus trackers form a stack
1354 // where the shorter lifetime trackers are at the top.
1355 while let Some(last) = invoker_contract_trackers.last() {
1356 // *Subtle*: there are two possible scenarios when tracker is considered
1357 // to be out of scope:
1358 // - When the sub-contract call is finished. For example, contract A creates
1359 // a tracker, then calls contract B. When we pop the frame of contract B,
1360 // the tracker's stack will be empty and thus considered to be out of scope.
1361 // - When the contract that created the tracker is going out of scope without
1362 // calling any sub-contracts. For example, contract A creates a tracker and
1363 // returns. When we pop the frame of contract A, the tracker's stack will be
1364 // empty (because it's created empty), and thus considered to be out of scope.
1365 // The invariant above is maintained in both scenarios.
1366 if last.is_out_of_scope() {
1367 invoker_contract_trackers.pop();
1368 } else {
1369 break;
1370 }
1371 }
1372 Ok(())
1373 }
1374
1375 // Returns the recorded per-address authorization payloads that would cover the
1376 // top-level contract function invocation in the enforcing mode.
1377 // Should only be called in the recording mode.
1378 // metering: free, recording mode
1379 #[cfg(any(test, feature = "recording_mode"))]
1380 pub(crate) fn get_recorded_auth_payloads(
1381 &self,
1382 host: &Host,
1383 ) -> Result<Vec<RecordedAuthPayload>, HostError> {
1384 match &self.mode {
1385 AuthorizationMode::Enforcing => Err(HostError::from((
1386 ScErrorType::Auth,
1387 ScErrorCode::InternalError,
1388 ))),
1389 AuthorizationMode::Recording(_) => Ok(self
1390 .try_borrow_account_trackers(host)?
1391 .iter()
1392 .map(|tracker| tracker.try_borrow_or_err()?.get_recorded_auth_payload(host))
1393 .collect::<Result<Vec<RecordedAuthPayload>, HostError>>()?),
1394 }
1395 }
1396
1397 // For recording mode, emulates authentication that would normally happen in
1398 // the enforcing mode.
1399 // This helps to build a more realistic footprint and produce more correct
1400 // meterting data for the recording mode.
1401 // No-op in the enforcing mode.
1402 // metering: covered
1403 #[cfg(any(test, feature = "recording_mode"))]
1404 pub(crate) fn maybe_emulate_authentication(&self, host: &Host) -> Result<(), HostError> {
1405 match &self.mode {
1406 AuthorizationMode::Enforcing => Ok(()),
1407 AuthorizationMode::Recording(_) => {
1408 for tracker in self.try_borrow_account_trackers(host)?.iter() {
1409 tracker
1410 .try_borrow_mut_or_err()?
1411 .emulate_authentication(host)?;
1412 }
1413 Ok(())
1414 }
1415 }
1416 }
1417
1418 // Returns a 'reset' instance of `AuthorizationManager` that has the same
1419 // mode, but no data.
1420 // metering: free, testutils
1421 #[cfg(any(test, feature = "testutils"))]
1422 pub(crate) fn reset(&mut self) {
1423 *self = match &self.mode {
1424 AuthorizationMode::Enforcing => {
1425 AuthorizationManager::new_enforcing_without_authorizations()
1426 }
1427 AuthorizationMode::Recording(rec_info) => {
1428 AuthorizationManager::new_recording(rec_info.disable_non_root_auth)
1429 }
1430 }
1431 }
1432
1433 // Returns all authorizations that have been authenticated for the
1434 // last contract invocation.
1435 // metering: free, testutils
1436 #[cfg(any(test, feature = "testutils"))]
1437 pub(crate) fn get_authenticated_authorizations(
1438 &self,
1439 host: &Host,
1440 ) -> Vec<(ScAddress, xdr::SorobanAuthorizedInvocation)> {
1441 host.as_budget()
1442 .with_observable_shadow_mode(|| {
1443 self.account_trackers
1444 .borrow()
1445 .iter()
1446 .filter(|t| t.borrow().verified)
1447 .map(|t| {
1448 (
1449 host.scaddress_from_address(t.borrow().address).unwrap(),
1450 t.borrow()
1451 .invocation_tracker
1452 .root_authorized_invocation
1453 .to_xdr(host, true)
1454 .unwrap(),
1455 )
1456 })
1457 .metered_collect(host)
1458 })
1459 .unwrap()
1460 }
1461}
1462
1463// Some helper extensions to support test-observation.
1464#[allow(dead_code)]
1465impl AuthorizationManager {
1466 pub(crate) fn stack_size(&self) -> usize {
1467 if let Ok(call_stack) = self.call_stack.try_borrow() {
1468 call_stack.len()
1469 } else {
1470 0
1471 }
1472 }
1473
1474 pub(crate) fn stack_hash(&self, budget: &Budget) -> Result<u64, HostError> {
1475 use std::hash::Hasher;
1476 if let Ok(call_stack) = self.call_stack.try_borrow() {
1477 let mut state = CountingHasher::default();
1478 call_stack.metered_hash(&mut state, budget)?;
1479 Ok(state.finish())
1480 } else {
1481 Ok(0)
1482 }
1483 }
1484
1485 pub(crate) fn trackers_hash_and_size(
1486 &self,
1487 budget: &Budget,
1488 ) -> Result<(u64, usize), HostError> {
1489 use std::hash::Hasher;
1490 let mut size: usize = 0;
1491 let mut state = CountingHasher::default();
1492 self.mode.metered_hash(&mut state, budget)?;
1493 if let Ok(account_trackers) = self.account_trackers.try_borrow() {
1494 for tracker in account_trackers.iter() {
1495 if let Ok(tracker) = tracker.try_borrow() {
1496 size = size.saturating_add(1);
1497 tracker.metered_hash(&mut state, budget)?;
1498 }
1499 }
1500 }
1501 if let Ok(invoker_contract_trackers) = self.invoker_contract_trackers.try_borrow() {
1502 for tracker in invoker_contract_trackers.iter() {
1503 size = size.saturating_add(1);
1504 tracker.metered_hash(&mut state, budget)?;
1505 }
1506 }
1507 Ok((state.finish(), size))
1508 }
1509}
1510
1511impl InvocationTracker {
1512 // metering: covered by components
1513 fn from_xdr(
1514 host: &Host,
1515 root_invocation: xdr::SorobanAuthorizedInvocation,
1516 ) -> Result<Self, HostError> {
1517 Ok(Self {
1518 root_authorized_invocation: AuthorizedInvocation::from_xdr(host, root_invocation)?,
1519 match_stack: vec![],
1520 root_exhausted_frame: None,
1521 is_fully_processed: false,
1522 })
1523 }
1524
1525 // metering: free
1526 fn new(root_authorized_invocation: AuthorizedInvocation) -> Self {
1527 Self {
1528 root_authorized_invocation,
1529 match_stack: vec![],
1530 root_exhausted_frame: None,
1531 is_fully_processed: false,
1532 }
1533 }
1534
1535 // metering: free for recording
1536 #[cfg(any(test, feature = "recording_mode"))]
1537 fn new_recording(function: AuthorizedFunction, current_stack_len: usize) -> Self {
1538 // Create the stack of `MatchState::Unmatched` leading to the current invocation to
1539 // represent invocations that didn't need authorization on behalf of
1540 // the tracked address.
1541 let mut match_stack = vec![MatchState::Unmatched; current_stack_len - 1];
1542 // Add a MatchState for the current(root) invocation.
1543 match_stack.push(MatchState::RootMatch);
1544 let root_exhausted_frame = Some(match_stack.len() - 1);
1545 Self {
1546 root_authorized_invocation: AuthorizedInvocation::new_recording(function),
1547 match_stack,
1548 root_exhausted_frame,
1549 is_fully_processed: false,
1550 }
1551 }
1552
1553 // Walks a path in the tree defined by `match_stack` and
1554 // returns the last visited authorized node.
1555 // metering: free
1556 fn last_authorized_invocation_mut(
1557 &mut self,
1558 ) -> Result<Option<&mut AuthorizedInvocation>, HostError> {
1559 for (i, m) in self.match_stack.iter().enumerate() {
1560 match m {
1561 MatchState::RootMatch => {
1562 return Ok(Some(
1563 self.root_authorized_invocation
1564 .last_authorized_invocation_mut(&self.match_stack, i + 1)?,
1565 ));
1566 }
1567 MatchState::SubMatch { .. } => {
1568 return Err((ScErrorType::Auth, ScErrorCode::InternalError).into())
1569 }
1570 MatchState::Unmatched => (),
1571 }
1572 }
1573 Ok(None)
1574 }
1575
1576 // metering: covered
1577 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1578 Vec::<usize>::charge_bulk_init_cpy(1, budget)?;
1579 self.match_stack.push(MatchState::Unmatched);
1580 Ok(())
1581 }
1582
1583 // metering: free
1584 fn pop_frame(&mut self) {
1585 self.match_stack.pop();
1586 if let Some(root_exhausted_frame) = self.root_exhausted_frame {
1587 if root_exhausted_frame >= self.match_stack.len() {
1588 self.is_fully_processed = true;
1589 }
1590 }
1591 }
1592
1593 // metering: free
1594 fn is_empty(&self) -> bool {
1595 self.match_stack.is_empty()
1596 }
1597
1598 // metering: free
1599 fn is_active(&self) -> bool {
1600 self.root_authorized_invocation.is_exhausted && !self.is_fully_processed
1601 }
1602
1603 // metering: free
1604 fn current_frame_is_already_matched(&self) -> bool {
1605 match self.match_stack.last() {
1606 Some(x) => x.is_matched(),
1607 _ => false,
1608 }
1609 }
1610
1611 // Tries to match the provided invocation as an extension of the last
1612 // currently-matched sub-invocation of the authorized invocation tree (or
1613 // the root, if there is no matched invocation yet). If matching succeeds,
1614 // it writes the match to the corresponding entry in
1615 // [`InvocationTracker::match_stack`].
1616 //
1617 // Returns `true` if the match has been found for the first time per current
1618 // frame.
1619 //
1620 // Metering: covered by components
1621 fn maybe_extend_invocation_match(
1622 &mut self,
1623 host: &Host,
1624 function: &AuthorizedFunction,
1625 allow_matching_root: bool,
1626 ) -> Result<bool, HostError> {
1627 if self.current_frame_is_already_matched() {
1628 return Ok(false);
1629 }
1630 let mut new_match_state = MatchState::Unmatched;
1631 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1632 for (index_in_parent, sub_invocation) in
1633 curr_invocation.sub_invocations.iter_mut().enumerate()
1634 {
1635 if !sub_invocation.is_exhausted
1636 && host.compare(&sub_invocation.function, function)?.is_eq()
1637 {
1638 new_match_state = MatchState::SubMatch { index_in_parent };
1639 sub_invocation.is_exhausted = true;
1640 break;
1641 }
1642 }
1643 } else if !self.root_authorized_invocation.is_exhausted
1644 && allow_matching_root
1645 && host
1646 .compare(&self.root_authorized_invocation.function, &function)?
1647 .is_eq()
1648 {
1649 new_match_state = MatchState::RootMatch;
1650 self.root_authorized_invocation.is_exhausted = true;
1651 self.root_exhausted_frame = Some(self.match_stack.len() - 1);
1652 }
1653 if new_match_state.is_matched() {
1654 *self.match_stack.last_mut().ok_or_else(|| {
1655 host.err(
1656 ScErrorType::Auth,
1657 ScErrorCode::InternalError,
1658 "invalid match_stack",
1659 &[],
1660 )
1661 })? = new_match_state;
1662 }
1663 Ok(new_match_state.is_matched())
1664 }
1665
1666 // Records the invocation in this tracker.
1667 // This is needed for the recording mode only.
1668 // This assumes that the address matching is correctly performed before
1669 // calling this.
1670 // metering: free for recording
1671 #[cfg(any(test, feature = "recording_mode"))]
1672 fn record_invocation(
1673 &mut self,
1674 host: &Host,
1675 function: AuthorizedFunction,
1676 ) -> Result<(), HostError> {
1677 if self.current_frame_is_already_matched() {
1678 return Err(host.err(
1679 ScErrorType::Auth,
1680 ScErrorCode::ExistingValue,
1681 "frame is already authorized",
1682 &[],
1683 ));
1684 }
1685 if let Some(curr_invocation) = self.last_authorized_invocation_mut()? {
1686 curr_invocation
1687 .sub_invocations
1688 .push(AuthorizedInvocation::new_recording(function));
1689 let index_in_parent = curr_invocation.sub_invocations.len() - 1;
1690 *self.match_stack.last_mut().unwrap() = MatchState::SubMatch { index_in_parent };
1691 } else {
1692 // This would be a bug
1693 return Err(host.err(
1694 ScErrorType::Auth,
1695 ScErrorCode::InternalError,
1696 "unexpected missing authorized invocation",
1697 &[],
1698 ));
1699 }
1700 Ok(())
1701 }
1702
1703 // metering: free
1704 #[cfg(any(test, feature = "recording_mode"))]
1705 fn has_matched_invocations_in_stack(&self) -> bool {
1706 self.match_stack.iter().any(|i| i.is_matched())
1707 }
1708
1709 // metering: covered
1710 fn snapshot(&self, budget: &Budget) -> Result<AuthorizedInvocationSnapshot, HostError> {
1711 self.root_authorized_invocation.snapshot(budget)
1712 }
1713
1714 // metering: covered
1715 fn rollback(&mut self, snapshot: &AuthorizedInvocationSnapshot) -> Result<(), HostError> {
1716 self.root_authorized_invocation.rollback(snapshot)?;
1717 // Invocation can only be rolled back from 'exhausted' to
1718 // 'non-exhausted' state (as there is no other way to go from
1719 // 'exhausted' state back to 'non-exhausted' state).
1720 if !self.root_authorized_invocation.is_exhausted {
1721 self.root_exhausted_frame = None;
1722 self.is_fully_processed = false;
1723 }
1724 Ok(())
1725 }
1726}
1727
1728impl AccountAuthorizationTracker {
1729 // Metering: covered by the host and components
1730 fn from_authorization_entry(
1731 host: &Host,
1732 auth_entry: SorobanAuthorizationEntry,
1733 ) -> Result<Self, HostError> {
1734 let (address, nonce, signature, is_transaction_source_account) =
1735 match auth_entry.credentials {
1736 SorobanCredentials::SourceAccount => (
1737 host.source_account_address()?.ok_or_else(|| {
1738 host.err(
1739 ScErrorType::Auth,
1740 ScErrorCode::InternalError,
1741 "source account is missing when setting auth entries",
1742 &[],
1743 )
1744 })?,
1745 None,
1746 Val::VOID.into(),
1747 true,
1748 ),
1749 SorobanCredentials::Address(address_creds) => (
1750 host.add_host_object(address_creds.address)?,
1751 Some((
1752 address_creds.nonce,
1753 address_creds.signature_expiration_ledger,
1754 )),
1755 host.to_host_val(&address_creds.signature)?,
1756 false,
1757 ),
1758 };
1759 Ok(Self {
1760 address,
1761 invocation_tracker: InvocationTracker::from_xdr(host, auth_entry.root_invocation)?,
1762 signature,
1763 verified: false,
1764 is_transaction_source_account,
1765 nonce,
1766 })
1767 }
1768
1769 // metering: free, since this is recording mode only
1770 #[cfg(any(test, feature = "recording_mode"))]
1771 fn new_recording(
1772 host: &Host,
1773 address: AddressObject,
1774 function: AuthorizedFunction,
1775 current_stack_len: usize,
1776 ) -> Result<Self, HostError> {
1777 if current_stack_len == 0 {
1778 // This would be a bug.
1779 return Err(host.err(
1780 ScErrorType::Auth,
1781 ScErrorCode::InternalError,
1782 "unexpected empty stack in recording auth",
1783 &[],
1784 ));
1785 }
1786 // Decide if we're tracking the transaction source account, and if so
1787 // don't bother with a nonce.
1788 let is_transaction_source_account =
1789 if let Some(source_acc) = host.source_account_address()? {
1790 host.compare(&source_acc, &address)?.is_eq()
1791 } else {
1792 false
1793 };
1794 let nonce = if !is_transaction_source_account {
1795 let random_nonce: i64 =
1796 host.with_recording_auth_nonce_prng(|p| Ok(p.gen_range(0..=i64::MAX)))?;
1797 // We use the `max_live_until_ledger` as the nonce lifetime here
1798 // in order to account for a maximum possible rent fee (given maximum
1799 // possible signature expiration). However, we don't want to actually
1800 // store that as nonce expiration ledger in the recording tracker,
1801 // as users are able (and encouraged) to customize the signature
1802 // expiration after simulation and before signing the auth payload.
1803 host.consume_nonce(address, random_nonce, host.max_live_until_ledger()?)?;
1804 Some((random_nonce, 0))
1805 } else {
1806 None
1807 };
1808 Ok(Self {
1809 address,
1810 invocation_tracker: InvocationTracker::new_recording(function, current_stack_len),
1811 signature: Val::VOID.into(),
1812 verified: true,
1813 is_transaction_source_account,
1814 nonce,
1815 })
1816 }
1817
1818 // Tries to find and enforce the provided invocation with this tracker and
1819 // lazily performs authentication when needed.
1820 // This is needed for the enforcing mode only.
1821 // This assumes that the address matching is correctly performed before
1822 // calling this.
1823 // Returns true/false based on whether the invocation is found in the
1824 // tracker. Returns error if invocation has been found, but the tracker
1825 // itself is not valid (failed authentication or nonce check).
1826 // metering: covered
1827 fn maybe_authorize_invocation(
1828 &mut self,
1829 host: &Host,
1830 function: &AuthorizedFunction,
1831 allow_matching_root: bool,
1832 ) -> Result<bool, HostError> {
1833 if !self.invocation_tracker.maybe_extend_invocation_match(
1834 host,
1835 function,
1836 allow_matching_root,
1837 )? {
1838 // The call isn't found in the currently tracked tree or is already
1839 // authorized in it.
1840 // That doesn't necessarily mean it's unauthorized (it can be
1841 // authorized in a different tracker).
1842 return Ok(false);
1843 }
1844 if !self.verified {
1845 let authenticate_res = self
1846 .authenticate(host)
1847 .map_err(|err| {
1848 // Convert any recoverable errors to auth errors so that it's
1849 // not possible to confuse them for the errors of the
1850 // contract that has called `require_auth`.
1851 // While there is no 'recovery' here, non-recoverable errors
1852 // aren't really useful for decoration.
1853 if err.is_recoverable() {
1854 // Also log the original error for diagnostics.
1855 host.err(
1856 ScErrorType::Auth,
1857 ScErrorCode::InvalidAction,
1858 "failed account authentication with error",
1859 &[self.address.into(), err.error.to_val()],
1860 )
1861 } else {
1862 err
1863 }
1864 })
1865 .and_then(|_| self.verify_and_consume_nonce(host));
1866 if let Some(err) = authenticate_res.err() {
1867 return Err(err);
1868 }
1869 self.verified = true;
1870 }
1871 Ok(true)
1872 }
1873
1874 // Records the invocation in this tracker.
1875 // This is needed for the recording mode only.
1876 // This assumes that the address matching is correctly performed before
1877 // calling this.
1878 // metering: free for recording
1879 #[cfg(any(test, feature = "recording_mode"))]
1880 fn record_invocation(
1881 &mut self,
1882 host: &Host,
1883 function: AuthorizedFunction,
1884 ) -> Result<(), HostError> {
1885 self.invocation_tracker.record_invocation(host, function)
1886 }
1887
1888 // Build the authorization payload from the invocations recorded in this
1889 // tracker.
1890 // metering: free for recording
1891 #[cfg(any(test, feature = "recording_mode"))]
1892 fn get_recorded_auth_payload(&self, host: &Host) -> Result<RecordedAuthPayload, HostError> {
1893 host.as_budget().with_observable_shadow_mode(|| {
1894 Ok(RecordedAuthPayload {
1895 address: if !self.is_transaction_source_account {
1896 Some(host.visit_obj(self.address, |a: &ScAddress| a.metered_clone(host))?)
1897 } else {
1898 None
1899 },
1900 invocation: self
1901 .invocation_tracker
1902 .root_authorized_invocation
1903 .to_xdr(host, false)?,
1904 nonce: self.nonce.map(|(nonce, _)| nonce),
1905 })
1906 })
1907 }
1908
1909 // Checks if there is at least one authorized invocation in the current call
1910 // stack.
1911 // metering: free
1912 #[cfg(any(test, feature = "recording_mode"))]
1913 fn has_authorized_invocations_in_stack(&self) -> bool {
1914 self.invocation_tracker.has_matched_invocations_in_stack()
1915 }
1916
1917 // metering: covered
1918 fn root_invocation_to_xdr(
1919 &self,
1920 host: &Host,
1921 ) -> Result<xdr::SorobanAuthorizedInvocation, HostError> {
1922 self.invocation_tracker
1923 .root_authorized_invocation
1924 .to_xdr(host, false)
1925 }
1926
1927 // metering: covered
1928 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
1929 self.invocation_tracker.push_frame(budget)
1930 }
1931
1932 // metering: covered
1933 fn pop_frame(&mut self) {
1934 self.invocation_tracker.pop_frame();
1935 }
1936
1937 // metering: covered
1938 fn verify_and_consume_nonce(&mut self, host: &Host) -> Result<(), HostError> {
1939 if self.is_transaction_source_account {
1940 return Ok(());
1941 }
1942 if let Some((nonce, live_until_ledger)) = &self.nonce {
1943 let ledger_seq = host.with_ledger_info(|li| Ok(li.sequence_number))?;
1944 if ledger_seq > *live_until_ledger {
1945 return Err(host.err(
1946 ScErrorType::Auth,
1947 ScErrorCode::InvalidInput,
1948 "signature has expired",
1949 &[
1950 self.address.into(),
1951 ledger_seq.try_into_val(host)?,
1952 live_until_ledger.try_into_val(host)?,
1953 ],
1954 ));
1955 }
1956 let max_live_until_ledger = host.max_live_until_ledger()?;
1957 if *live_until_ledger > max_live_until_ledger {
1958 return Err(host.err(
1959 ScErrorType::Auth,
1960 ScErrorCode::InvalidInput,
1961 "signature expiration is too late",
1962 &[
1963 self.address.into(),
1964 max_live_until_ledger.try_into_val(host)?,
1965 live_until_ledger.try_into_val(host)?,
1966 ],
1967 ));
1968 }
1969
1970 return host.consume_nonce(self.address, *nonce, *live_until_ledger);
1971 }
1972 Err(host.err(
1973 ScErrorType::Auth,
1974 ScErrorCode::InternalError,
1975 "unexpected nonce verification state",
1976 &[],
1977 ))
1978 }
1979
1980 // Computes the payload that has to be signed in order to authenticate
1981 // the authorized invocation tree corresponding to this tracker.
1982 // metering: covered by components
1983 fn get_signature_payload(&self, host: &Host) -> Result<[u8; 32], HostError> {
1984 let (nonce, live_until_ledger) = self.nonce.ok_or_else(|| {
1985 host.err(
1986 ScErrorType::Auth,
1987 ScErrorCode::InternalError,
1988 "unexpected missing nonce",
1989 &[],
1990 )
1991 })?;
1992 let payload_preimage =
1993 HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
1994 network_id: Hash(host.with_ledger_info(|li| li.network_id.metered_clone(host))?),
1995 nonce,
1996 signature_expiration_ledger: live_until_ledger,
1997 invocation: self.root_invocation_to_xdr(host)?,
1998 });
1999
2000 host.metered_hash_xdr(&payload_preimage)
2001 }
2002
2003 // metering: covered by the hsot
2004 fn authenticate(&self, host: &Host) -> Result<(), HostError> {
2005 if self.is_transaction_source_account {
2006 return Ok(());
2007 }
2008
2009 let sc_addr = host.scaddress_from_address(self.address)?;
2010 // TODO: there should also be a mode where a dummy payload is used
2011 // instead (for enforcing mode preflight).
2012 let payload = self.get_signature_payload(host)?;
2013 match sc_addr {
2014 ScAddress::Account(acc) => {
2015 check_account_authentication(host, acc, &payload, self.signature)?;
2016 }
2017 ScAddress::Contract(acc_contract) => {
2018 check_account_contract_auth(
2019 host,
2020 &acc_contract,
2021 &payload,
2022 self.signature,
2023 &self.invocation_tracker.root_authorized_invocation,
2024 )?;
2025 }
2026 }
2027 Ok(())
2028 }
2029
2030 // Emulates authentication for the recording mode.
2031 // metering: covered
2032 #[cfg(any(test, feature = "recording_mode"))]
2033 fn emulate_authentication(&mut self, host: &Host) -> Result<(), HostError> {
2034 if self.is_transaction_source_account {
2035 return Ok(());
2036 }
2037 let sc_addr = host.scaddress_from_address(self.address)?;
2038 match sc_addr {
2039 ScAddress::Account(acc) => {
2040 // Emulate verification of a single signature that belongs to this
2041 // account.
2042 // We could emulate more (up to 20) signature verifications, but
2043 // since signature verification is a pretty expensive operation, while
2044 // multisig in combination with Soroban auth is probably pretty rare,
2045 // multisig users should either use enforcing auth simulation, or
2046 // intentionally increase the instruction count on the recording result.
2047 let key_bytes = match &acc.0 {
2048 PublicKey::PublicKeyTypeEd25519(k) => k.0,
2049 };
2050 let signature = AccountEd25519Signature {
2051 public_key: BytesN::from_slice(host, &key_bytes)?,
2052 signature: BytesN::from_slice(host, &[0_u8; 64])?,
2053 };
2054 let signatures = host_vec![host, signature]?;
2055 self.signature = signatures.into();
2056 // Authentication is expected to fail here after signature verification,
2057 // so we suppress the error and diagnostics.
2058 host.with_suppressed_diagnostic_events(|| {
2059 let _ = self.authenticate(host);
2060 Ok(())
2061 })?;
2062
2063 // Emulate a clone of the account, which serves 2 purposes:
2064 // - Account for metered clone in `get_signer_weight_from_account`
2065 // - Return budget error in case if it was suppressed above.
2066 let _ = acc.metered_clone(host.as_budget())?;
2067 }
2068 // Skip custom accounts for now - emulating authentication for
2069 // them requires a dummy signature.
2070 ScAddress::Contract(_) => {}
2071 }
2072 Ok(())
2073 }
2074
2075 // metering: covered
2076 fn snapshot(&self, budget: &Budget) -> Result<AccountAuthorizationTrackerSnapshot, HostError> {
2077 Ok(AccountAuthorizationTrackerSnapshot {
2078 invocation_tracker_root_snapshot: self.invocation_tracker.snapshot(budget)?,
2079 // The verification status can be rolled back in case
2080 // of a contract failure. In case if the root call has
2081 // failed, the nonce will get 'un-consumed' due to storage
2082 // rollback. Thus we need to run verification and consume
2083 // it again in case if the call is retried. Another subtle
2084 // case where this behavior is important is the case when
2085 // custom account's authentication function depends on
2086 // some ledger state which might get modified in-between calls.
2087 verified: self.verified,
2088 })
2089 }
2090
2091 // metering: covered
2092 fn rollback(
2093 &mut self,
2094 snapshot: &AccountAuthorizationTrackerSnapshot,
2095 ) -> Result<(), HostError> {
2096 self.invocation_tracker
2097 .rollback(&snapshot.invocation_tracker_root_snapshot)?;
2098 self.verified = snapshot.verified;
2099 Ok(())
2100 }
2101
2102 // metering: free
2103 fn is_active(&self) -> bool {
2104 self.invocation_tracker.is_active()
2105 }
2106
2107 // metering: free
2108 fn current_frame_is_already_matched(&self) -> bool {
2109 self.invocation_tracker.current_frame_is_already_matched()
2110 }
2111}
2112
2113impl InvokerContractAuthorizationTracker {
2114 // metering: covered by components
2115 fn new_with_curr_contract_as_invoker(
2116 host: &Host,
2117 invoker_auth_entry: Val,
2118 ) -> Result<Self, HostError> {
2119 let invoker_sc_addr = ScAddress::Contract(host.get_current_contract_id_internal()?);
2120 let authorized_invocation = invoker_contract_auth_to_authorized_invocation(
2121 host,
2122 &invoker_sc_addr,
2123 invoker_auth_entry,
2124 )?;
2125 let invocation_tracker = InvocationTracker::new(authorized_invocation);
2126 Ok(Self {
2127 contract_address: host.add_host_object(invoker_sc_addr)?,
2128 invocation_tracker,
2129 })
2130 }
2131
2132 // metering: covered
2133 fn push_frame(&mut self, budget: &Budget) -> Result<(), HostError> {
2134 self.invocation_tracker.push_frame(budget)
2135 }
2136
2137 // metering: covered
2138 fn pop_frame(&mut self) {
2139 self.invocation_tracker.pop_frame();
2140 }
2141
2142 // metering: free
2143 fn is_out_of_scope(&self) -> bool {
2144 self.invocation_tracker.is_empty()
2145 }
2146
2147 // metering: covered
2148 fn maybe_authorize_invocation(
2149 &mut self,
2150 host: &Host,
2151 function: &AuthorizedFunction,
2152 ) -> Result<bool, HostError> {
2153 // Authorization is successful if function is just matched by the
2154 // tracker. No authentication is needed.
2155 self.invocation_tracker
2156 .maybe_extend_invocation_match(host, function, true)
2157 }
2158}
2159
2160impl Host {
2161 // metering: covered by components
2162 fn consume_nonce(
2163 &self,
2164 address: AddressObject,
2165 nonce: i64,
2166 live_until_ledger: u32,
2167 ) -> Result<(), HostError> {
2168 let nonce_key_scval = ScVal::LedgerKeyNonce(ScNonceKey { nonce });
2169 let sc_address = self.scaddress_from_address(address)?;
2170 let nonce_key = self.storage_key_for_address(
2171 sc_address.metered_clone(self)?,
2172 nonce_key_scval.metered_clone(self)?,
2173 xdr::ContractDataDurability::Temporary,
2174 )?;
2175 let live_until_ledger = live_until_ledger
2176 .max(self.get_min_live_until_ledger(xdr::ContractDataDurability::Temporary)?);
2177 self.with_mut_storage(|storage| {
2178 if storage
2179 .has_with_host(&nonce_key, self, None)
2180 .map_err(|err| {
2181 if err.error.is_type(ScErrorType::Storage)
2182 && err.error.is_code(ScErrorCode::ExceededLimit)
2183 {
2184 return self.err(
2185 ScErrorType::Storage,
2186 ScErrorCode::ExceededLimit,
2187 "trying to access nonce outside of footprint for address",
2188 &[address.to_val()],
2189 );
2190 }
2191 err
2192 })?
2193 {
2194 return Err(self.err(
2195 ScErrorType::Auth,
2196 ScErrorCode::ExistingValue,
2197 "nonce already exists for address",
2198 &[address.into()],
2199 ));
2200 }
2201 let data = LedgerEntryData::ContractData(ContractDataEntry {
2202 contract: sc_address,
2203 key: nonce_key_scval,
2204 val: ScVal::Void,
2205 durability: xdr::ContractDataDurability::Temporary,
2206 ext: xdr::ExtensionPoint::V0,
2207 });
2208 let entry = LedgerEntry {
2209 last_modified_ledger_seq: 0,
2210 data,
2211 ext: LedgerEntryExt::V0,
2212 };
2213 storage.put_with_host(
2214 &nonce_key,
2215 &Rc::metered_new(entry, self)?,
2216 Some(live_until_ledger),
2217 self,
2218 None,
2219 )
2220 })
2221 }
2222
2223 // Returns the recorded per-address authorization payloads that would cover the
2224 // top-level contract function invocation in the enforcing mode.
2225 // This should only be called in the recording authorization mode, i.e. only
2226 // if `switch_to_recording_auth` has been called.
2227 #[cfg(any(test, feature = "recording_mode"))]
2228 pub fn get_recorded_auth_payloads(&self) -> Result<Vec<RecordedAuthPayload>, HostError> {
2229 #[cfg(not(any(test, feature = "testutils")))]
2230 {
2231 self.try_borrow_authorization_manager()?
2232 .get_recorded_auth_payloads(self)
2233 }
2234 #[cfg(any(test, feature = "testutils"))]
2235 {
2236 let payloads = self
2237 .try_borrow_previous_authorization_manager()?
2238 .as_ref()
2239 .ok_or_else(|| {
2240 self.err(
2241 ScErrorType::Auth,
2242 ScErrorCode::InvalidAction,
2243 "previous invocation is missing - no auth data to get",
2244 &[],
2245 )
2246 })?
2247 .get_recorded_auth_payloads(self)?;
2248 Ok(payloads)
2249 }
2250 }
2251}
2252
2253#[cfg(any(test, feature = "testutils"))]
2254use crate::{host::frame::ContractReentryMode, xdr::SorobanAuthorizedInvocation};
2255
2256#[cfg(any(test, feature = "testutils"))]
2257impl Host {
2258 /// Invokes the reserved `__check_auth` function on a provided contract.
2259 ///
2260 /// This is useful for testing the custom account contracts. Otherwise, the
2261 /// host prohibits calling `__check_auth` outside of internal implementation
2262 /// of `require_auth[_for_args]` calls.
2263 pub fn call_account_contract_check_auth(
2264 &self,
2265 contract: AddressObject,
2266 args: VecObject,
2267 ) -> Result<Val, HostError> {
2268 use crate::builtin_contracts::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME;
2269 let contract_id = self.contract_id_from_address(contract)?;
2270 let args_vec = self.call_args_from_obj(args)?;
2271 let res = self.call_n_internal(
2272 &contract_id,
2273 ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(self)?,
2274 args_vec.as_slice(),
2275 ContractReentryMode::Prohibited,
2276 true,
2277 );
2278 if let Err(e) = &res {
2279 self.error(
2280 e.error,
2281 "check auth invocation for a custom account contract failed",
2282 &[contract.to_val(), args.to_val()],
2283 );
2284 }
2285 res
2286 }
2287
2288 /// Returns the current state of the authorization manager.
2289 ///
2290 /// Use this in conjunction with `set_auth_manager` to do authorized
2291 /// operations without breaking the current authorization state (useful for
2292 /// preserving the auth state while doing the generic test setup).
2293 pub fn snapshot_auth_manager(&self) -> Result<AuthorizationManager, HostError> {
2294 Ok(self.try_borrow_authorization_manager()?.clone())
2295 }
2296
2297 /// Replaces authorization manager with the provided new instance.
2298 ///
2299 /// Use this in conjunction with `snapshot_auth_manager` to do authorized
2300 /// operations without breaking the current authorization state (useful for
2301 /// preserving the auth state while doing the generic test setup).
2302 pub fn set_auth_manager(&self, auth_manager: AuthorizationManager) -> Result<(), HostError> {
2303 *self.try_borrow_authorization_manager_mut()? = auth_manager;
2304 Ok(())
2305 }
2306
2307 // Returns the authorizations that have been authenticated for the last
2308 // contract invocation.
2309 //
2310 // Authenticated means that either the authorization was authenticated using
2311 // the actual authorization logic for that authorization in enforced mode,
2312 // or that it was recorded in recording mode and authorization was assumed
2313 // successful.
2314 pub fn get_authenticated_authorizations(
2315 &self,
2316 ) -> Result<Vec<(ScAddress, SorobanAuthorizedInvocation)>, HostError> {
2317 Ok(self
2318 .try_borrow_previous_authorization_manager_mut()?
2319 .as_mut()
2320 .map(|am| am.get_authenticated_authorizations(self))
2321 // If no AuthorizationManager is setup, no authorizations could have
2322 // taken place so return an empty vec.
2323 .unwrap_or_default())
2324 }
2325}
2326
2327// metering: free for testutils
2328#[cfg(any(test, feature = "testutils"))]
2329impl PartialEq for RecordedAuthPayload {
2330 fn eq(&self, other: &Self) -> bool {
2331 self.address == other.address
2332 && self.invocation == other.invocation
2333 // Compare nonces only by presence of the value - recording mode
2334 // generates random nonces.
2335 && self.nonce.is_some() == other.nonce.is_some()
2336 }
2337}