use crate::frame_result::FrameResult;
use crate::mock_contract::{MockContract, MockContractInner};
use crate::mock_transaction_handler::MockTransactionHandler;
use crate::test_environment_builder::TestEnvironmentBuilder;
use crate::transact_result::TransactResult;
use crate::transaction_builder::TransactionBuilder;
use core::ffi::c_void;
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::rc::Rc;
use thruput_storage_cost::StorageCostModel;
use thruput_sys::FFIErrorCode;
use thruput_trace::ffi_enums::InvokeErrorCode;
use thruput_xdr::address_auth::AddressAuthData;
use thruput_xdr::contract::ContractEntrypointID;
use thruput_xdr::contract::*;
use thruput_xdr::network_config::NetworkConfig;
use thruput_xdr::storage_item::WireStorageItem;
use thruput_xdr::transaction::*;
use thruput_xdr::types::*;
use xdrs::txs;
const TEST_CONTRACT_SIZE: u32 = 1024;
pub const TEST_NETWORK_NAME: &str = "LOCAL_UNIT_TEST";
pub fn test_network_hash() -> NetworkHash {
let mut hasher = Sha256::new();
hasher.update(TEST_NETWORK_NAME.as_bytes());
let hash: [u8; 32] = hasher.finalize().into();
NetworkHash::new(hash.into())
}
pub(crate) struct TestEnvironmentInner {
pub(crate) db: DBWrapper,
pub(crate) contract_context: RuntimeContextWrapper,
mock_contract_index: i32,
contracts: Vec<*mut MockContractInner>, }
pub struct TestEnvironment {
pub(crate) inner: RefCell<TestEnvironmentInner>,
}
unsafe extern "C" fn trampoline(
context: *mut c_void,
method: u64,
) -> thruput_sys::ForeignInvokeResult {
let c: *mut MockContractInner = unsafe { core::mem::transmute(context) };
let method: ContractEntrypointID = method.to_be_bytes().into();
match unsafe { (*c).get_func(method).or_else(|| (*c).get_unknown_func()) }
{
Some(x) => {
let _in_body = crate::invoke_error::PanicHookGuard::enter();
x()
}
None => thruput_sys::ForeignInvokeResult {
panic: InvokeErrorCode::Deterministic.0,
},
}
}
pub(crate) struct DBWrapper {
pub(crate) db: *mut c_void,
}
impl DBWrapper {
pub fn new(
cost_model: Option<&StorageCostModel>,
network_hash: &NetworkHash,
genesis_root_contract: &Contract,
network_config: Option<&NetworkConfig>,
) -> Self {
let cost_model_bytes =
cost_model.map(|model| model.serialize().unwrap());
let ffi_cost_model_bytes: (*const u8, u32) = cost_model_bytes
.as_ref()
.map(|vec| (vec.as_ptr(), vec.len().try_into().unwrap()))
.unwrap_or((core::ptr::null(), 0));
let genesis_root_bytes = txs::encode(genesis_root_contract).unwrap();
let network_config_bytes =
network_config.map(|config| txs::encode(config).unwrap());
let ffi_network_config_bytes: (*const u8, u32) = network_config_bytes
.as_ref()
.map(|vec| (vec.as_ptr(), vec.len().try_into().unwrap()))
.unwrap_or((core::ptr::null(), 0));
let res = unsafe {
thruput_sys::create_db(
ffi_cost_model_bytes.0,
ffi_cost_model_bytes.1,
network_hash.as_ptr(),
genesis_root_bytes.as_ptr(),
genesis_root_bytes.len().try_into().unwrap(),
ffi_network_config_bytes.0,
ffi_network_config_bytes.1,
)
};
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to create db: {}", res.ffi_error_code);
}
assert!(!res.db.is_null());
Self { db: res.db }
}
pub fn get_cost_model(&self) -> StorageCostModel {
let res = unsafe { thruput_sys::get_cost_model(self.db) };
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to get cost model: {}", res.ffi_error_code);
}
if res.data.len == 0 {
panic!("failed to get cost model: no data");
}
let data = xdrs::txs::decode::<
thruput_xdr::storage_cost::StorageCostModelXdr,
>(res.data.as_slice().unwrap())
.unwrap();
StorageCostModel::new(data)
}
pub fn get_network_config(&self) -> NetworkConfig {
let res = unsafe { thruput_sys::get_network_config(self.db) };
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to get network config: {}", res.ffi_error_code);
}
if res.data.len == 0 {
panic!("failed to get network config: no data");
}
xdrs::txs::decode::<NetworkConfig>(res.data.as_slice().unwrap())
.unwrap()
}
pub fn get_snapshot_storage_item(
&self,
addr: &ContractAddress,
key: &StorageKeyContractData,
) -> Option<WireStorageItem> {
self.get_snapshot_item(
addr,
StorageKey::ContractData {
contract_data_key: *key,
},
)
}
pub fn get_snapshot_auth_data(
&self,
addr: &ContractAddress,
) -> Option<AddressAuthData> {
match self.get_snapshot_item(addr, StorageKey::AuthData)? {
WireStorageItem::kItemTypeAuthData { auth_data } => {
Some(auth_data)
}
other => panic!("auth data key holds a non-auth item: {other:?}"),
}
}
fn get_snapshot_item(
&self,
addr: &ContractAddress,
key: StorageKey,
) -> Option<WireStorageItem> {
let addrkey = AddressAndKey {
address: *addr,
key,
};
let serialized_addrkey = txs::encode(&addrkey).unwrap();
let ffi_addrkey = thruput_sys::FFISerializedAddressAndKey {
data: serialized_addrkey.as_ptr(),
len: serialized_addrkey.len().try_into().unwrap(),
};
let res = unsafe {
thruput_sys::get_snapshot_storage_item(self.db, ffi_addrkey)
};
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to get snapshot storage item: {err}");
}
res.data
.as_slice()
.map(|bytes| txs::decode::<WireStorageItem>(bytes).unwrap())
}
}
impl Drop for DBWrapper {
fn drop(&mut self) {
let r = unsafe { thruput_sys::free_db(self.db) };
let err: FFIErrorCode = r.into();
if err != FFIErrorCode::None {
crate::drop_failure(format_args!("failed to free db: {:?}", err));
}
}
}
pub(crate) struct RuntimeContextWrapper {
pub context: *mut c_void,
}
impl RuntimeContextWrapper {
pub fn new() -> Self {
let res = unsafe { thruput_sys::open_runtime_context() };
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to create runtime context: {}", res.ffi_error_code);
}
assert!(!res.context.is_null());
Self {
context: res.context,
}
}
}
impl Drop for RuntimeContextWrapper {
fn drop(&mut self) {
let r = unsafe { thruput_sys::close_runtime_context(self.context) };
let err: FFIErrorCode = r.into();
if err != FFIErrorCode::None {
crate::drop_failure(format_args!(
"failed to close runtime context: {:?}",
err
));
}
}
}
impl TestEnvironmentInner {
pub fn new(
cost_model: Option<&StorageCostModel>,
network_hash: &NetworkHash,
genesis_root_contract: &Contract,
network_config: Option<&NetworkConfig>,
) -> Self {
Self {
db: DBWrapper::new(
cost_model,
network_hash,
genesis_root_contract,
network_config,
),
contract_context: RuntimeContextWrapper::new(),
mock_contract_index: 0,
contracts: vec![],
}
}
pub fn enable_syscall_logging(level: thruput_sys::FFILogLevel) {
unsafe { thruput_sys::enable_syscall_logging(level as i32) };
}
pub fn setup_create_contract(
&mut self,
contract: &Contract,
) -> SHA256Hash {
let contract_bytes = txs::encode(&contract).unwrap();
let res = unsafe {
thruput_sys::setup_create_contract(
self.contract_context.context,
self.db.db,
contract_bytes.as_ptr(),
contract_bytes.len().try_into().unwrap(),
)
};
if res != 0 {
let err: thruput_sys::FFIErrorCode = res.into();
if err == thruput_sys::FFIErrorCode::ContractVerificationFailed {
panic!(
"setup_create_contract failed: {err}\n\
\n\
The contract's rv32 artifact failed validation. Common causes:\n\
- The raw ELF was deployed instead of the lowered .rv32a image\n\
- Memory image bounds too large\n\
- Unsupported instructions (only the RV32IM subset is accepted)\n"
);
}
panic!("setup_create_contract failed: {err}");
}
let mut hasher = Sha256::new();
hasher.update(&contract_bytes);
let res: [u8; 32] = hasher.finalize().into();
res.into()
}
pub fn setup_create_deployment(
&mut self,
addr: &ContractAddress,
contract: &Contract,
) {
let contract_bytes = txs::encode(&contract).unwrap();
let mut hasher = Sha256::new();
hasher.update(&contract_bytes);
let res: [u8; 32] = hasher.finalize().into();
let contract_hash: SHA256Hash = res.into();
let contract_size = deployment_contract_size(contract);
let res = unsafe {
thruput_sys::setup_deploy_contract(
self.db.db,
addr.as_ptr(),
contract_hash.as_ptr(),
contract_size,
)
};
let err: FFIErrorCode = res.into();
if err != FFIErrorCode::None {
panic!("failed to setup new contract deployment: {:?}", err);
}
}
pub fn setup_grant_storage_tokens(
&mut self,
addr: &ContractAddress,
tokens: i64,
) {
let res = unsafe {
thruput_sys::setup_grant_contract_storage_tokens(
self.db.db,
addr.as_ptr(),
tokens,
)
};
let err: FFIErrorCode = res.into();
if err != FFIErrorCode::None {
panic!("failed to setup grant storage tokens: {:?}", err);
}
}
pub fn advance_block(&mut self) {
self.advance_block_inner(std::ptr::null())
}
pub fn advance_block_at(&mut self, block_timepoint: TimePoint) {
let raw: u64 = block_timepoint.into_inner();
self.advance_block_inner(&raw)
}
fn advance_block_inner(&mut self, block_timepoint: *const u64) {
let res = unsafe {
thruput_sys::advance_new_block(self.db.db, block_timepoint)
};
let err: FFIErrorCode = res.into();
if err == FFIErrorCode::InvalidArgument {
panic!(
"failed to advance block: the requested timepoint precedes the \
block it would follow. Blocks only move forward in time; pin a \
timepoint at or after the current block's."
);
}
if err != FFIErrorCode::None {
panic!("failed to advance block: {:?}", err);
}
}
pub fn add_contract(&mut self) -> *mut MockContractInner {
let out: *mut MockContractInner = Box::into_raw(Box::new(
MockContractInner::new(self.mock_contract_index),
));
let new_id = self.mock_contract_index;
self.contracts.push(out);
self.mock_contract_index += 1;
let res = unsafe {
thruput_sys::register_new_contract(
self.contract_context.context,
new_id,
Some(trampoline),
out as *mut c_void,
)
};
let err: FFIErrorCode = res.into();
if err != FFIErrorCode::None {
panic!("failed to link fn: {:?}", err);
}
out
}
pub fn get_current_cost_model(&self) -> StorageCostModel {
self.db.get_cost_model()
}
pub fn get_network_config(&self) -> NetworkConfig {
self.db.get_network_config()
}
pub fn run_auth_checks(
&self,
tx: &SignedTransaction,
) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
let serialized_tx = txs::encode(tx).unwrap();
let ffi_serialized_tx = thruput_sys::FFISerializedSignedTransaction {
data: serialized_tx.as_ptr() as *mut u8,
len: serialized_tx.len().try_into().unwrap(),
};
let res = unsafe {
thruput_sys::verify_signed_transaction_auth_from_db(
self.db.db,
ffi_serialized_tx,
)
};
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to run auth checks: {:?}", err);
}
res.auth_error_code.into()
}
}
impl Drop for TestEnvironmentInner {
fn drop(&mut self) {
for contract in &self.contracts {
let _ = unsafe { Box::from_raw(*contract) };
}
}
}
impl Default for TestEnvironmentInner {
fn default() -> Self {
Self::new(
None,
&test_network_hash(),
&mock_genesis_root_contract(),
None,
)
}
}
pub type TestEnvironmentHandle = Rc<TestEnvironment>;
pub fn mock_genesis_root_contract() -> Contract {
Contract::kMockTest {
mock_contract_id: u32::MAX as i32,
}
}
impl TestEnvironment {
pub fn new(
cost_model: Option<&StorageCostModel>,
) -> TestEnvironmentHandle {
Self::with_network_hash(cost_model, &test_network_hash())
}
pub fn with_network_hash(
cost_model: Option<&StorageCostModel>,
network_hash: &NetworkHash,
) -> TestEnvironmentHandle {
Self::with_genesis_root_contract(
cost_model,
network_hash,
&mock_genesis_root_contract(),
)
}
pub fn with_genesis_root_contract(
cost_model: Option<&StorageCostModel>,
network_hash: &NetworkHash,
genesis_root_contract: &Contract,
) -> TestEnvironmentHandle {
Self::from_parts(cost_model, network_hash, genesis_root_contract, None)
}
pub fn builder() -> TestEnvironmentBuilder {
TestEnvironmentBuilder::new()
}
pub(crate) fn from_parts(
cost_model: Option<&StorageCostModel>,
network_hash: &NetworkHash,
genesis_root_contract: &Contract,
network_config: Option<&NetworkConfig>,
) -> TestEnvironmentHandle {
let te = Rc::new(Self {
inner: RefCell::new(TestEnvironmentInner::new(
cost_model,
network_hash,
genesis_root_contract,
network_config,
)),
});
if std::env::var("SDK_TEST_ENABLE_SYSCALL_LOGGING").is_ok() {
Self::enable_syscall_logging();
}
te
}
#[allow(clippy::should_implement_trait)]
pub fn default() -> TestEnvironmentHandle {
Self::new(None)
}
pub fn no_cost() -> TestEnvironmentHandle {
Self::new(Some(
&thruput_storage_cost::StorageCostModel::no_cost_model(),
))
}
pub fn enable_syscall_logging() {
TestEnvironmentInner::enable_syscall_logging(
thruput_sys::FFILogLevel::Trace,
);
}
pub fn advance_block(self: &TestEnvironmentHandle) {
self.inner.borrow_mut().advance_block()
}
pub fn advance_block_at(
self: &TestEnvironmentHandle,
block_timepoint: TimePoint,
) {
self.inner.borrow_mut().advance_block_at(block_timepoint)
}
pub fn get_current_cost_model(
self: &TestEnvironmentHandle,
) -> StorageCostModel {
self.inner.borrow().get_current_cost_model()
}
pub fn get_network_config(self: &TestEnvironmentHandle) -> NetworkConfig {
self.inner.borrow().get_network_config()
}
pub fn get_snapshot_storage_item(
self: &TestEnvironmentHandle,
addr: &ContractAddress,
key: &StorageKeyContractData,
) -> Option<WireStorageItem> {
self.inner.borrow().db.get_snapshot_storage_item(addr, key)
}
pub fn get_snapshot_auth_data(
self: &TestEnvironmentHandle,
addr: &ContractAddress,
) -> Option<AddressAuthData> {
self.inner.borrow().db.get_snapshot_auth_data(addr)
}
#[doc(hidden)]
pub fn add_contract(self: &TestEnvironmentHandle) -> MockContract {
let inner = self.inner.borrow_mut().add_contract();
MockContract::new(inner, self.clone())
}
pub fn register_mock<F>(self: &TestEnvironmentHandle, f: F) -> MockContract
where
F: FnOnce(&mut MockContract),
{
let mut c = self.add_contract();
f(&mut c);
self.setup_create_contract(&c.as_contract());
c
}
pub fn transaction(self: &TestEnvironmentHandle) -> TransactionBuilder {
TransactionBuilder::new(self.clone())
}
#[must_use]
pub fn transact(
self: &TestEnvironmentHandle,
f: impl FnOnce(&mut MockTransactionHandler) -> FrameResult,
) -> TransactResult {
self.transaction().transact(f)
}
pub fn transact_manual<R>(
self: &TestEnvironmentHandle,
f: impl FnOnce(&mut MockTransactionHandler) -> R,
) -> R {
self.transaction().transact_manual(f)
}
pub fn setup_create_contract(
self: &TestEnvironmentHandle,
contract: &Contract,
) -> SHA256Hash {
self.inner.borrow_mut().setup_create_contract(contract)
}
pub fn setup_create_deployment(
self: &TestEnvironmentHandle,
addr: &ContractAddress,
contract: &Contract,
) {
self.inner
.borrow_mut()
.setup_create_deployment(addr, contract)
}
pub fn setup_grant_storage_tokens(
self: &TestEnvironmentHandle,
addr: &ContractAddress,
tokens: i64,
) {
self.inner
.borrow_mut()
.setup_grant_storage_tokens(addr, tokens)
}
pub fn verify_signed_transaction_auth(
self: &TestEnvironmentHandle,
tx: &SignedTransaction,
) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
self.inner.borrow().run_auth_checks(tx)
}
pub fn verify_signed_transaction_auth_static(
tx: &SignedTransaction,
source_auth_data: Option<&AddressAuthData>,
network_hash: &NetworkHash,
) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
let serialized_tx = txs::encode(tx).unwrap();
let ffi_serialized_tx = thruput_sys::FFISerializedSignedTransaction {
data: serialized_tx.as_ptr() as *mut u8,
len: serialized_tx.len().try_into().unwrap(),
};
let serialized_source_auth_data =
source_auth_data.map(|d| txs::encode(d).unwrap());
let ffi_serialized_source_auth_data =
match &serialized_source_auth_data {
Some(bytes) => thruput_sys::FFISourceAuthData {
data: bytes.as_ptr() as *mut u8,
len: bytes.len().try_into().unwrap(),
},
None => thruput_sys::FFISourceAuthData {
data: core::ptr::null_mut(),
len: 0,
},
};
let res = unsafe {
thruput_sys::verify_signed_transaction_auth(
ffi_serialized_tx,
ffi_serialized_source_auth_data,
network_hash.as_ptr(),
)
};
let err: FFIErrorCode = res.ffi_error_code.into();
if err != FFIErrorCode::None {
panic!("failed to run auth checks: {:?}", err);
}
res.auth_error_code.into()
}
}
fn deployment_contract_size(contract: &Contract) -> u32 {
match contract {
Contract::kNative { .. } => 0,
Contract::kMockTest { .. } => TEST_CONTRACT_SIZE,
Contract::kRv32 { rv32_contract } => rv32_contract.data.len() as u32,
other => panic!("unsupported contract variant: {:?}", other),
}
}
#[cfg(test)]
mod tests {
use super::*;
use xdrs::BytesM;
const TEST_ADDR: ContractAddress = ContractAddress::new([0x11; 32]);
#[test]
fn test_network_hash_is_sha256_of_the_test_network_name() {
let expected: [u8; 32] = [
0xb0, 0xb2, 0xfc, 0xc8, 0x57, 0xda, 0xa6, 0x04, 0x78, 0x7b, 0xb8,
0x4b, 0x45, 0xa4, 0xca, 0xf0, 0x32, 0xea, 0x6b, 0x93, 0xf5, 0xa5,
0xed, 0x1c, 0x14, 0xa9, 0xbf, 0xf8, 0xfa, 0x65, 0xbb, 0x52,
];
assert_eq!(test_network_hash(), NetworkHash::new(expected.into()));
}
#[test]
fn deployment_contract_size_uses_rv32_blob_length() {
let contract = Contract::kRv32 {
rv32_contract: ContractBlob {
data: BytesM(vec![1, 2, 3, 4, 5]),
},
};
assert_eq!(deployment_contract_size(&contract), 5);
}
#[test]
fn get_snapshot_storage_item_reads_committed_bytestring() {
let env = TestEnvironment::new(None);
let key = StorageKeyContractData::new([0x22; 32]);
env.setup_grant_storage_tokens(&TEST_ADDR, 1_000_000_000);
let result = env.transact(|tx| {
tx.call(&TEST_ADDR).run(|| {
thruput_xdr::syscalls::raw_BytestringSet(&key, b"hello")
.unwrap();
})
});
result.assert_success();
env.advance_block();
let item = env.get_snapshot_storage_item(&TEST_ADDR, &key);
assert_eq!(
item,
Some(WireStorageItem::kItemTypeBytestring {
bytestring: b"hello".to_vec().try_into().unwrap(),
}),
);
}
#[test]
fn get_snapshot_storage_item_returns_none_for_missing_key() {
let env = TestEnvironment::new(None);
let key = StorageKeyContractData::new([0x33; 32]);
assert_eq!(env.get_snapshot_storage_item(&TEST_ADDR, &key), None);
}
#[test]
fn get_snapshot_storage_item_reads_committed_bytestring_on_deployed_address()
{
let env = TestEnvironment::new(None);
let key = StorageKeyContractData::new([0x44; 32]);
env.setup_grant_storage_tokens(&TEST_ADDR, 1_000_000_000);
let _mock = env.inner.borrow_mut().add_contract();
let contract = thruput_xdr::contract::Contract::kMockTest {
mock_contract_id: 0,
};
env.inner.borrow_mut().setup_create_contract(&contract);
env.inner
.borrow_mut()
.setup_create_deployment(&TEST_ADDR, &contract);
let result = env.transact(|tx| {
tx.call(&TEST_ADDR).run(|| {
thruput_xdr::syscalls::raw_BytestringSet(&key, b"hello")
.unwrap();
})
});
result.assert_success();
env.advance_block();
let item = env.get_snapshot_storage_item(&TEST_ADDR, &key);
assert_eq!(
item,
Some(WireStorageItem::kItemTypeBytestring {
bytestring: b"hello".to_vec().try_into().unwrap(),
}),
);
}
}