use super::contract_result::*;
use super::encode::*;
use super::raw_call_error::*;
use crate::contract::ContractEntrypointID;
use crate::exn::{FaultInfo, FaultString, RawExceptionRef};
use crate::types::transaction::InvocationFrameStatusCode;
use crate::types::{ContractAddress, RuntimeMemoryLength};
use crate::xdr::*;
use crate::{AuthHandleBuf, sys};
#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
pub fn try_call_raw_with<ARGS, RET, X>(
address: &ContractAddress,
entrypoint: ContractEntrypointID,
gas_limit: u64,
args: &ARGS,
flags: crate::invoke_config::InvokeFlagSet,
on_exn: impl for<'s> FnOnce(RawExceptionRef<'s>) -> X,
) -> Result<RET, RawCallError<X>>
where
ARGS: WriteXDR + crate::CollectAuthHandles,
RET: ReadXDROwned + crate::ReceiveAuthHandles,
{
with_call_scratch(|buf, auth_buf| {
encode_args_into(buf, args).map_err(widen_uninhabited_exn)?;
args.collect_auth_handles(auth_buf)
.map_err(RawCallError::EncodingArgs)?;
let (is_ok, returned_auth_tokens) =
call_core(address, entrypoint, gas_limit, flags, buf, auth_buf)
.map_err(widen_uninhabited_exn)?;
if is_ok {
contract_call_decode(buf.as_slice(), returned_auth_tokens)
.map_err(widen_uninhabited_exn)
} else {
match RawExceptionRef::parse(buf.as_slice()) {
Ok(exn) => Err(RawCallError::Exception(on_exn(exn))),
Err(e) => Err(RawCallError::UnparseableAbort(
FaultString::from_display(e),
)),
}
}
})
}
fn encode_args_into<T: WriteXDR + ?Sized>(
buf: &mut Buf,
value: &T,
) -> Result<(), CallFault> {
crate::internal::encode_scratch(buf, value)
.map_err(|e| RawCallError::EncodingArgs(FaultString::from_display(e)))
}
#[inline(never)]
fn call_core(
address: &ContractAddress,
entrypoint: ContractEntrypointID,
gas_limit: u64,
flags: crate::invoke_config::InvokeFlagSet,
buf: &mut Buf,
auth_buf: &AuthHandleBuf,
) -> Result<(bool, core::ops::Range<u32>), CallFault> {
let (is_ok, outputs) = invoke_frame(
address,
entrypoint,
gas_limit,
buf.as_slice(),
auth_buf,
flags,
)?;
buf.clear();
let returned_auth_tokens = fetch_frame_payload(is_ok, &outputs, buf)?;
Ok((is_ok, returned_auth_tokens))
}
#[inline]
fn fetch_frame_payload(
is_ok: bool,
outputs: &sys::CopyableInvokeOutputs,
buf: &mut Buf,
) -> Result<core::ops::Range<u32>, CallFault> {
fill_return_buf(
outputs.return_buffer_len,
outputs.returned_auth_token_count,
buf,
)
.map_err(|fault| unparseable(is_ok, fault))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallOutcome {
Return,
Abort,
}
#[allow(clippy::too_many_arguments)]
pub fn contract_call<A>(
address: &ContractAddress,
entrypoint: ContractEntrypointID,
gas_limit: u64,
args: &A,
auth_token_ids: &crate::collect_auth_handles::AuthHandleBuf,
flags: crate::invoke_config::InvokeFlagSet,
return_buf: &mut ReturnBuf,
returned_auth_token_ids: &mut AuthHandleBuf,
) -> Result<CallOutcome, CallFault>
where
A: AsRef<[u8]> + ?Sized,
{
return_buf.clear();
returned_auth_token_ids.clear();
let (is_ok, outputs) = invoke_frame(
address,
entrypoint,
gas_limit,
args.as_ref(),
auth_token_ids,
flags,
)?;
#[cfg(feature = "alloc")]
return_buf.reserve(outputs.return_buffer_len as usize);
if let Err(e) =
fetch_frame_payload(is_ok, &outputs, return_buf).and_then(|returned| {
push_auth_handles(returned_auth_token_ids, returned)
})
{
return_buf.clear();
returned_auth_token_ids.clear();
return Err(e);
}
Ok(if is_ok {
CallOutcome::Return
} else {
CallOutcome::Abort
})
}
pub(crate) fn widen_uninhabited_exn<X>(e: CallFault) -> RawCallError<X> {
match e {
RawCallError::Invalid => RawCallError::Invalid,
RawCallError::Reentrance => RawCallError::Reentrance,
RawCallError::DepthExceeded => RawCallError::DepthExceeded,
RawCallError::InvokeRejected(c) => RawCallError::InvokeRejected(c),
RawCallError::Exception(x) => match x {},
RawCallError::Fault(f) => RawCallError::Fault(f),
RawCallError::UnparseableReturn(f) => {
RawCallError::UnparseableReturn(f)
}
RawCallError::UnparseableAbort(f) => RawCallError::UnparseableAbort(f),
RawCallError::UnparseableAuthHandles(f) => {
RawCallError::UnparseableAuthHandles(f)
}
RawCallError::EncodingArgs(f) => RawCallError::EncodingArgs(f),
}
}
fn unparseable(is_ok: bool, fault: FaultString) -> CallFault {
if is_ok {
RawCallError::UnparseableReturn(fault)
} else {
RawCallError::UnparseableAbort(fault)
}
}
fn invoke_frame(
address: &ContractAddress,
entrypoint: ContractEntrypointID,
gas_limit: u64,
args: &[u8],
auth_token_ids: &crate::collect_auth_handles::AuthHandleBuf,
flags: crate::invoke_config::InvokeFlagSet,
) -> Result<(bool, sys::CopyableInvokeOutputs), CallFault> {
let entrypoint = u64::from_be_bytes(entrypoint.into_inner());
let auth_bytes = auth_handles_as_bytes(auth_token_ids.as_slice());
let inputs = sys::CopyableInvokeInputs {
entrypoint_id: entrypoint,
gas_limit,
flags,
reserved: 0,
};
let mut outputs = sys::CopyableInvokeOutputs {
return_buffer_len: 0,
returned_auth_token_count: 0,
frame_status: InvocationFrameStatusCode::Fault,
};
let result = {
let _reset = crate::storage::live_guards::FrameGuardReset::new();
sys::raw_Invoke(address, args, auth_bytes, &inputs, &mut outputs)
};
let is_ok = match result {
Ok(()) => {
let status = outputs.frame_status;
if status == InvocationFrameStatusCode::Return {
true
} else if status == InvocationFrameStatusCode::Abort {
false
} else {
return Err(RawCallError::Fault(FaultInfo::empty()));
}
}
Err(syserr) => return Err(classify_invoke_rejection(syserr)),
};
Ok((is_ok, outputs))
}
fn push_auth_handles(
buf: &mut AuthHandleBuf,
ids: core::ops::Range<u32>,
) -> Result<(), CallFault> {
#[cfg(feature = "alloc")]
{
buf.extend(ids);
Ok(())
}
#[cfg(not(feature = "alloc"))]
{
for id in ids {
buf.push(id).map_err(|_| {
RawCallError::UnparseableAuthHandles(
FaultString::from_display(
"auth handle buffer capacity exceeded",
),
)
})?;
}
Ok(())
}
}
#[cfg(feature = "alloc")]
use crate::cell::{ContractLocalRef, ContractRefCell};
struct CallScratch {
buf: Buf,
auth: AuthHandleBuf,
}
#[cfg(not(feature = "alloc"))]
fn with_call_scratch<R>(
f: impl FnOnce(&mut Buf, &mut AuthHandleBuf) -> R,
) -> R {
let mut scratch = fresh_call_scratch();
f(&mut scratch.buf, &mut scratch.auth)
}
#[cfg(feature = "alloc")]
fn with_call_scratch<R>(
f: impl FnOnce(&mut Buf, &mut AuthHandleBuf) -> R,
) -> R {
let mut scratch = SCRATCH_POOL
.with_ref(|pool| pool.borrow_mut().pop())
.unwrap_or_else(fresh_call_scratch);
reset_call_scratch(&mut scratch);
let out = f(&mut scratch.buf, &mut scratch.auth);
SCRATCH_POOL.with_ref(|pool| pool.borrow_mut().push(scratch));
out
}
#[cfg(feature = "alloc")]
crate::contract_local! {
static SCRATCH_POOL: ContractRefCell<alloc::vec::Vec<CallScratch>> =
const { ContractRefCell::new(alloc::vec::Vec::new()) };
}
#[cfg(feature = "alloc")]
#[inline]
fn reset_call_scratch(scratch: &mut CallScratch) {
scratch.buf.clear();
scratch.auth.clear();
if scratch.buf.capacity() < crate::internal::SCRATCH_MAX {
scratch.buf.reserve(crate::internal::SCRATCH_MAX);
}
}
fn fresh_call_scratch() -> CallScratch {
CallScratch {
buf: crate::internal::new_scratch(),
auth: AuthHandleBuf::new(),
}
}
pub(crate) fn with_scratch<R>(f: impl FnOnce(&mut Buf) -> R) -> R {
with_call_scratch(|buf, _auth| f(buf))
}
#[cfg(all(test, not(target_arch = "riscv32"), feature = "std"))]
mod scratch_tests {
use super::*;
#[test]
fn nested_scratch_uses_get_distinct_buffers() {
with_scratch(|outer| {
outer.clear();
outer.extend_from_slice(&[0xAA; 8]);
let outer_ptr = outer.as_slice().as_ptr();
let inner_ptr = with_scratch(|inner| {
inner.clear();
inner.extend_from_slice(&[0xBB; 8]);
inner.as_slice().as_ptr()
});
assert_ne!(
outer_ptr, inner_ptr,
"nested scratch use aliased the outer buffer"
);
assert_eq!(outer.as_slice(), &[0xAA; 8]);
});
}
#[test]
#[cfg(feature = "alloc")]
fn sequential_scratch_uses_reuse_one_buffer() {
let first = with_scratch(|b| {
b.reserve(crate::internal::SCRATCH_MAX);
b.as_slice().as_ptr()
});
let second = with_scratch(|b| b.as_slice().as_ptr());
assert_eq!(
first, second,
"scratch buffer was not returned to the pool"
);
}
}
#[inline]
fn fill_return_buf(
return_len: u32,
returned_auth_token_count: u32,
return_buf: &mut ReturnBuf,
) -> Result<core::ops::Range<u32>, FaultString> {
let mut written_mu: core::mem::MaybeUninit<RuntimeMemoryLength> =
core::mem::MaybeUninit::uninit();
let mut start_mu: core::mem::MaybeUninit<u32> =
core::mem::MaybeUninit::uninit();
let mut count_mu: core::mem::MaybeUninit<u32> =
core::mem::MaybeUninit::uninit();
sys::uninit_GetReturnedBuffer(
return_buf.spare_capacity_mut(),
&mut written_mu,
&mut start_mu,
&mut count_mu,
)
.map_err(|syserr| FaultString::from_display(format_args!("{syserr:?}")))?;
let written = unsafe { written_mu.assume_init() };
let written =
checked_returned_len(written, return_len, return_buf.capacity())?;
unsafe {
return_buf.set_len(written);
}
returned_auth_token_range(
unsafe { start_mu.assume_init() },
unsafe { count_mu.assume_init() },
returned_auth_token_count,
)
}
fn checked_returned_len(
written: RuntimeMemoryLength,
return_len: u32,
capacity: usize,
) -> Result<usize, FaultString> {
if written != return_len {
return Err(FaultString::from_display(format_args!(
"unexpected return-data length from system call: \
written = {written}, \
return_len = {return_len}"
)));
}
let written = written as usize;
if written > capacity {
return Err(FaultString::from_display(format_args!(
"return-data length {written} exceeds return buffer \
capacity {capacity}"
)));
}
Ok(written)
}
fn returned_auth_token_range(
start: u32,
count: u32,
invoke_count: u32,
) -> Result<core::ops::Range<u32>, FaultString> {
if count != invoke_count {
return Err(FaultString::from_display(format_args!(
"unexpected returned auth token count from system call: \
GetReturnedBuffer = {count}, Invoke = {invoke_count}"
)));
}
let end = start.checked_add(count).ok_or_else(|| {
FaultString::from_display(format_args!(
"returned auth token ids overflow: start = {start}, \
count = {count}"
))
})?;
Ok(start..end)
}
#[cfg(test)]
mod tests {
use super::checked_returned_len;
const CAPACITY: usize = 500;
#[test]
fn oversized_return_len_is_rejected() {
assert!(checked_returned_len(600, 600, CAPACITY).is_err());
}
#[test]
fn return_len_at_capacity_is_accepted() {
assert_eq!(
checked_returned_len(CAPACITY as u32, CAPACITY as u32, CAPACITY)
.unwrap(),
CAPACITY
);
}
#[test]
fn return_len_within_capacity_is_accepted() {
assert_eq!(checked_returned_len(480, 480, CAPACITY).unwrap(), 480);
}
#[test]
fn empty_return_is_accepted() {
assert_eq!(checked_returned_len(0, 0, CAPACITY).unwrap(), 0);
}
#[test]
fn mismatched_written_and_return_len_is_rejected() {
assert!(checked_returned_len(400, 500, CAPACITY).is_err());
}
}