Skip to main content

oasis_runtime_sdk/
subcall.rs

1//! Subcall dispatch.
2use std::cell::RefCell;
3
4use crate::{
5    context::Context,
6    dispatcher,
7    module::CallResult,
8    modules::core::{Error, API as _},
9    runtime::Runtime,
10    state::{CurrentState, Options, TransactionResult, TransactionWithMeta},
11    types::{token::BaseUnits, transaction, transaction::CallerAddress},
12};
13
14thread_local! {
15    /// The subcall stack for tracking depth and other metadata.
16    static SUBCALL_STACK: RefCell<SubcallStack> = RefCell::new(SubcallStack::new());
17}
18
19/// Subcall validator.
20pub trait Validator {
21    /// Validate a subcall before it is performed.
22    fn validate(&self, info: &SubcallInfo) -> Result<(), Error>;
23}
24
25/// A validator which allows everything.
26pub struct AllowAllValidator;
27
28impl Validator for AllowAllValidator {
29    fn validate(&self, _info: &SubcallInfo) -> Result<(), Error> {
30        Ok(())
31    }
32}
33
34/// Information about a subcall to be dispatched.
35#[derive(Clone, Debug)]
36pub struct SubcallInfo {
37    /// Address of the caller.
38    pub caller: CallerAddress,
39    /// Method to call.
40    pub method: String,
41    /// Subcall body.
42    pub body: cbor::Value,
43    /// Maximum subcall depth.
44    pub max_depth: u16,
45    /// Maximum gas amount that can be consumed.
46    pub max_gas: u64,
47    /// Whether the subcall is read-only.
48    pub read_only: bool,
49}
50
51/// Result of dispatching a subcall.
52#[derive(Debug)]
53pub struct SubcallResult {
54    /// Result of the subcall.
55    pub call_result: CallResult,
56    /// Gas used by the subcall.
57    pub gas_used: u64,
58}
59
60struct SubcallStackEntry {
61    validator: Box<dyn Validator>,
62}
63
64struct SubcallStack {
65    stack: Vec<SubcallStackEntry>,
66}
67
68impl SubcallStack {
69    fn new() -> Self {
70        Self { stack: Vec::new() }
71    }
72
73    fn depth(&self) -> u16 {
74        self.stack.len() as u16
75    }
76
77    fn push(&mut self, entry: SubcallStackEntry) {
78        self.stack.push(entry);
79    }
80
81    fn pop(&mut self) {
82        self.stack.pop();
83    }
84
85    fn run_validators(&self, info: &SubcallInfo) -> Result<(), Error> {
86        for entry in &self.stack {
87            entry.validator.validate(info)?;
88        }
89        Ok(())
90    }
91}
92
93struct SubcallStackGuard;
94
95impl Drop for SubcallStackGuard {
96    fn drop(&mut self) {
97        SUBCALL_STACK.with(|ss| {
98            ss.borrow_mut().pop();
99        });
100    }
101}
102
103/// The current subcall depth.
104pub fn get_current_subcall_depth<C: Context>(_ctx: &C) -> u16 {
105    SUBCALL_STACK.with(|ss| ss.borrow().depth())
106}
107
108/// Perform a subcall.
109pub fn call<C: Context, V: Validator + 'static>(
110    ctx: &C,
111    info: SubcallInfo,
112    validator: V,
113) -> Result<SubcallResult, Error> {
114    // Run validator first.
115    validator.validate(&info)?;
116
117    // Update the subcall stack after doing validation.
118    SUBCALL_STACK.with(|ss| {
119        let mut stack = ss.borrow_mut();
120
121        // Ensure the call depth is not too large.
122        if stack.depth() >= info.max_depth {
123            return Err(Error::CallDepthExceeded(stack.depth() + 1, info.max_depth));
124        }
125
126        // Run existing validators.
127        stack.run_validators(&info)?;
128
129        // Push subcall to stack.
130        stack.push(SubcallStackEntry {
131            validator: Box::new(validator) as Box<dyn Validator>,
132        });
133
134        Ok(())
135    })?;
136    let _guard = SubcallStackGuard; // Ensure subcall is popped from stack.
137
138    // Generate an internal transaction.
139    let tx = transaction::Transaction {
140        version: transaction::LATEST_TRANSACTION_VERSION,
141        call: transaction::Call {
142            format: transaction::CallFormat::Plain,
143            method: info.method,
144            body: info.body,
145            read_only: info.read_only,
146        },
147        auth_info: transaction::AuthInfo {
148            signer_info: vec![transaction::SignerInfo {
149                // The call is being performed on the caller's behalf.
150                address_spec: transaction::AddressSpec::Internal(info.caller),
151                nonce: 0,
152            }],
153            fee: transaction::Fee {
154                amount: BaseUnits::native(0),
155                // Limit gas usage inside the child context to the allocated maximum.
156                gas: info.max_gas,
157                // Propagate consensus message limit.
158                consensus_messages: CurrentState::with(|state| state.emitted_messages_max(ctx)),
159                proxy: None,
160            },
161            ..Default::default()
162        },
163    };
164    let call = tx.call.clone(); // TODO: Avoid clone.
165
166    // Execute a transaction in a child context.
167    let (call_result, gas) = CurrentState::with_transaction_opts(
168        Options::new()
169            .with_internal(true)
170            .with_tx(TransactionWithMeta::internal(tx)),
171        || {
172            // Dispatch the call.
173            let (result, _) = dispatcher::Dispatcher::<C::Runtime>::dispatch_tx_call(
174                &ctx.clone(), // Must clone to avoid infinite type.
175                call,
176                &Default::default(),
177            );
178            // Retrieve remaining gas.
179            let gas = <C::Runtime as Runtime>::Core::remaining_tx_gas();
180
181            // Commit store and return emitted tags and messages on successful dispatch,
182            // otherwise revert state and ignore any emitted events/messages.
183            if result.is_success() {
184                TransactionResult::Commit((result, gas))
185            } else {
186                // Ignore tags/messages on failure.
187                TransactionResult::Rollback((result, gas))
188            }
189        },
190    );
191
192    // Compute the amount of gas used.
193    let gas_used = info.max_gas.saturating_sub(gas);
194
195    Ok(SubcallResult {
196        call_result,
197        gas_used,
198    })
199}