Skip to main content

thruput_native_exec/
test_environment.rs

1use crate::frame_result::FrameResult;
2use crate::mock_contract::{MockContract, MockContractInner};
3use crate::mock_transaction_handler::MockTransactionHandler;
4use crate::test_environment_builder::TestEnvironmentBuilder;
5use crate::transact_result::TransactResult;
6use crate::transaction_builder::TransactionBuilder;
7use core::ffi::c_void;
8use sha2::{Digest, Sha256};
9use std::cell::RefCell;
10use std::rc::Rc;
11use thruput_storage_cost::StorageCostModel;
12use thruput_sys::FFIErrorCode;
13use thruput_trace::ffi_enums::InvokeErrorCode;
14use thruput_xdr::address_auth::AddressAuthData;
15use thruput_xdr::contract::ContractEntrypointID;
16use thruput_xdr::contract::*;
17use thruput_xdr::network_config::NetworkConfig;
18use thruput_xdr::storage_item::WireStorageItem;
19use thruput_xdr::transaction::*;
20use thruput_xdr::types::*;
21use xdrs::txs;
22
23// When deploying a new contract, hogd accounts for the storage cost of the contract
24// based on its size.  We don't have access to contract size here (because it's not built),
25// so we pick a default (nonzero) value as a placeholder.
26// Nonzero so that contract tests will expect to at least pay some storage cost when deploying a new contract.
27const TEST_CONTRACT_SIZE: u32 = 1024;
28
29/// Name of the network that [`TestEnvironment`]s run on.
30///
31/// Every environment is created against one fixed, obviously-not-mainnet
32/// network identity, so that a test cannot accidentally produce (or replay)
33/// something signed for a real network.
34pub const TEST_NETWORK_NAME: &str = "LOCAL_UNIT_TEST";
35
36/// The [`NetworkHash`] that [`TestEnvironment`]s are created with:
37/// `sha256(`[`TEST_NETWORK_NAME`]`)`.
38///
39/// Pass a different one to [`TestEnvironment::with_network_hash`] if a test
40/// needs to pin a specific network.
41pub fn test_network_hash() -> NetworkHash {
42    let mut hasher = Sha256::new();
43    hasher.update(TEST_NETWORK_NAME.as_bytes());
44    let hash: [u8; 32] = hasher.finalize().into();
45    NetworkHash::new(hash.into())
46}
47
48/// The state behind a [`TestEnvironment`]: the FFI database, runtime
49/// context, and registered mock contracts.
50pub(crate) struct TestEnvironmentInner {
51    pub(crate) db: DBWrapper,
52    pub(crate) contract_context: RuntimeContextWrapper,
53    mock_contract_index: i32,
54    contracts: Vec<*mut MockContractInner>, // owns these contracts
55}
56
57/// An in-process Thruput instance for testing contracts natively.
58///
59/// A `TestEnvironment` owns a fresh in-memory database and runtime context
60/// (backed by hogd via FFI). Constructors return a [`TestEnvironmentHandle`];
61/// clone the handle to share the environment across helpers.
62///
63/// - [`TestEnvironment::new`] takes an optional storage cost model;
64///   [`TestEnvironment::no_cost`] disables storage costs entirely. Both run on
65///   the default test network ([`test_network_hash`]);
66///   [`TestEnvironment::with_network_hash`] picks a different one.
67/// - The `setup_*` methods seed state (contracts, deployments, storage
68///   tokens) as if applied in a previous block; call them before running
69///   transactions.
70/// - [`TestEnvironment::transact`] executes a transaction against the
71///   environment and auto-commits; build frames with the fluent
72///   `tx.call(&addr)` API. [`TestEnvironment::advance_block`] makes
73///   committed effects visible to subsequent transactions.
74/// - [`TestEnvironment::builder`] reaches settings the constructors above
75///   do not, notably the chain's network config parameters. It is entirely
76///   optional: everything else here behaves the same whether or not any
77///   test uses it.
78///
79/// ```no_run
80/// use thruput_native_exec::test_environment::TestEnvironment;
81/// use thruput_xdr::types::ContractAddress;
82///
83/// let env = TestEnvironment::no_cost();
84/// let addr = ContractAddress::new([0x11; 32]);
85/// let result = env.transact(|tx| tx.call(&addr).run(|| { /* guest code */ }));
86/// result.assert_success();
87/// ```
88///
89/// Setting the `SDK_TEST_ENABLE_SYSCALL_LOGGING` environment variable turns
90/// on syscall tracing for environments created afterwards.
91pub struct TestEnvironment {
92    pub(crate) inner: RefCell<TestEnvironmentInner>,
93}
94
95unsafe extern "C" fn trampoline(
96    context: *mut c_void,
97    method: u64,
98) -> thruput_sys::ForeignInvokeResult {
99    let c: *mut MockContractInner = unsafe { core::mem::transmute(context) };
100
101    let method: ContractEntrypointID = method.to_be_bytes().into();
102
103    // Undefined method: run the registered fallback if there is one (see
104    // `MockContract::register_unknown_method`), otherwise fail the frame
105    // deterministically — the native analog of the rv32 dispatcher's
106    // `ebreak`.
107    match unsafe { (*c).get_func(method).or_else(|| (*c).get_unknown_func()) }
108    {
109        Some(x) => {
110            // Mark this thread as running a contract body for the call. The
111            // body may resume on a pool thread rather than the one that
112            // activated the panic hook, and this is the point where the right
113            // thread is known without guessing: it is the one calling `x`.
114            let _in_body = crate::invoke_error::PanicHookGuard::enter();
115            x()
116        }
117        None => thruput_sys::ForeignInvokeResult {
118            panic: InvokeErrorCode::Deterministic.0,
119        },
120    }
121}
122
123pub(crate) struct DBWrapper {
124    pub(crate) db: *mut c_void,
125}
126
127impl DBWrapper {
128    pub fn new(
129        cost_model: Option<&StorageCostModel>,
130        network_hash: &NetworkHash,
131        genesis_root_contract: &Contract,
132        network_config: Option<&NetworkConfig>,
133    ) -> Self {
134        let cost_model_bytes =
135            cost_model.map(|model| model.serialize().unwrap());
136
137        let ffi_cost_model_bytes: (*const u8, u32) = cost_model_bytes
138            .as_ref()
139            .map(|vec| (vec.as_ptr(), vec.len().try_into().unwrap()))
140            .unwrap_or((core::ptr::null(), 0));
141
142        let genesis_root_bytes = txs::encode(genesis_root_contract).unwrap();
143
144        let network_config_bytes =
145            network_config.map(|config| txs::encode(config).unwrap());
146        let ffi_network_config_bytes: (*const u8, u32) = network_config_bytes
147            .as_ref()
148            .map(|vec| (vec.as_ptr(), vec.len().try_into().unwrap()))
149            .unwrap_or((core::ptr::null(), 0));
150
151        // hogd reads exactly 32 bytes from this pointer (it is not XDR), and
152        // rejects a null one. A null network config takes this build's stock
153        // genesis values; a non-null one must state a gapless prefix of the
154        // parameter id space, which hogd checks fatally (SIGABRT, no return
155        // through the FFI) -- build it with
156        // `crate::network_config::NetworkConfigBuilder`, which refuses to
157        // produce anything else.
158        let res = unsafe {
159            thruput_sys::create_db(
160                ffi_cost_model_bytes.0,
161                ffi_cost_model_bytes.1,
162                network_hash.as_ptr(),
163                genesis_root_bytes.as_ptr(),
164                genesis_root_bytes.len().try_into().unwrap(),
165                ffi_network_config_bytes.0,
166                ffi_network_config_bytes.1,
167            )
168        };
169        let err: FFIErrorCode = res.ffi_error_code.into();
170        if err != FFIErrorCode::None {
171            panic!("failed to create db: {}", res.ffi_error_code);
172        }
173        assert!(!res.db.is_null());
174        Self { db: res.db }
175    }
176
177    pub fn get_cost_model(&self) -> StorageCostModel {
178        let res = unsafe { thruput_sys::get_cost_model(self.db) };
179        let err: FFIErrorCode = res.ffi_error_code.into();
180        if err != FFIErrorCode::None {
181            panic!("failed to get cost model: {}", res.ffi_error_code);
182        }
183        if res.data.len == 0 {
184            panic!("failed to get cost model: no data");
185        }
186
187        let data = xdrs::txs::decode::<
188            thruput_xdr::storage_cost::StorageCostModelXdr,
189        >(res.data.as_slice().unwrap())
190        .unwrap();
191        StorageCostModel::new(data)
192    }
193
194    pub fn get_network_config(&self) -> NetworkConfig {
195        let res = unsafe { thruput_sys::get_network_config(self.db) };
196        let err: FFIErrorCode = res.ffi_error_code.into();
197        if err != FFIErrorCode::None {
198            panic!("failed to get network config: {}", res.ffi_error_code);
199        }
200        if res.data.len == 0 {
201            panic!("failed to get network config: no data");
202        }
203
204        xdrs::txs::decode::<NetworkConfig>(res.data.as_slice().unwrap())
205            .unwrap()
206    }
207
208    pub fn get_snapshot_storage_item(
209        &self,
210        addr: &ContractAddress,
211        key: &StorageKeyContractData,
212    ) -> Option<WireStorageItem> {
213        self.get_snapshot_item(
214            addr,
215            StorageKey::ContractData {
216                contract_data_key: *key,
217            },
218        )
219    }
220
221    pub fn get_snapshot_auth_data(
222        &self,
223        addr: &ContractAddress,
224    ) -> Option<AddressAuthData> {
225        match self.get_snapshot_item(addr, StorageKey::AuthData)? {
226            WireStorageItem::kItemTypeAuthData { auth_data } => {
227                Some(auth_data)
228            }
229            other => panic!("auth data key holds a non-auth item: {other:?}"),
230        }
231    }
232
233    fn get_snapshot_item(
234        &self,
235        addr: &ContractAddress,
236        key: StorageKey,
237    ) -> Option<WireStorageItem> {
238        let addrkey = AddressAndKey {
239            address: *addr,
240            key,
241        };
242        let serialized_addrkey = txs::encode(&addrkey).unwrap();
243        let ffi_addrkey = thruput_sys::FFISerializedAddressAndKey {
244            data: serialized_addrkey.as_ptr(),
245            len: serialized_addrkey.len().try_into().unwrap(),
246        };
247
248        let res = unsafe {
249            thruput_sys::get_snapshot_storage_item(self.db, ffi_addrkey)
250        };
251        let err: FFIErrorCode = res.ffi_error_code.into();
252        if err != FFIErrorCode::None {
253            panic!("failed to get snapshot storage item: {err}");
254        }
255
256        res.data
257            .as_slice()
258            .map(|bytes| txs::decode::<WireStorageItem>(bytes).unwrap())
259    }
260}
261
262impl Drop for DBWrapper {
263    fn drop(&mut self) {
264        let r = unsafe { thruput_sys::free_db(self.db) };
265        let err: FFIErrorCode = r.into();
266        if err != FFIErrorCode::None {
267            crate::drop_failure(format_args!("failed to free db: {:?}", err));
268        }
269    }
270}
271
272pub(crate) struct RuntimeContextWrapper {
273    pub context: *mut c_void,
274}
275
276impl RuntimeContextWrapper {
277    pub fn new() -> Self {
278        let res = unsafe { thruput_sys::open_runtime_context() };
279        let err: FFIErrorCode = res.ffi_error_code.into();
280        if err != FFIErrorCode::None {
281            panic!("failed to create runtime context: {}", res.ffi_error_code);
282        }
283        assert!(!res.context.is_null());
284        Self {
285            context: res.context,
286        }
287    }
288}
289
290impl Drop for RuntimeContextWrapper {
291    fn drop(&mut self) {
292        let r = unsafe { thruput_sys::close_runtime_context(self.context) };
293        let err: FFIErrorCode = r.into();
294        if err != FFIErrorCode::None {
295            crate::drop_failure(format_args!(
296                "failed to close runtime context: {:?}",
297                err
298            ));
299        }
300    }
301}
302
303impl TestEnvironmentInner {
304    /// Create a fresh in-memory database (with the given cost model) and
305    /// runtime context on `network_hash`. `genesis_root_contract` is
306    /// installed as the genesis root deployment, and `network_config`
307    /// states the execution parameters -- `None` for the stock ones.
308    pub fn new(
309        cost_model: Option<&StorageCostModel>,
310        network_hash: &NetworkHash,
311        genesis_root_contract: &Contract,
312        network_config: Option<&NetworkConfig>,
313    ) -> Self {
314        // the initialization in create_db() advances the block once, so it is prepped for
315        // accepting new transactions (in proposal mode)
316        Self {
317            db: DBWrapper::new(
318                cost_model,
319                network_hash,
320                genesis_root_contract,
321                network_config,
322            ),
323            contract_context: RuntimeContextWrapper::new(),
324            mock_contract_index: 0,
325            contracts: vec![],
326        }
327    }
328
329    /// Turn on FFI-level syscall logging to stderr at `level` for
330    /// environments created afterwards.
331    pub fn enable_syscall_logging(level: thruput_sys::FFILogLevel) {
332        unsafe { thruput_sys::enable_syscall_logging(level as i32) };
333    }
334
335    /// Creates a new contract within Thruput.
336    /// This new contract is not deployed at any particular address.
337    ///
338    /// In the test framework, the semantics of the setup_{create,deploy}_contract functions
339    /// are that after the function is called, the effects on the database are as though that
340    /// contract had been {created,deployed} during the previous block.
341    ///
342    /// This contract can be subsequently deployed, either by the setup_create_deployment function,
343    /// or by the syscalls within a transaction.
344    ///
345    /// Returns the hash of the contract, which is used to identify the contract when deploying it.
346    ///
347    /// Note: Even if a mock contract is added to the test environment (via add_contract),
348    /// this function must be called to create the contract within Thruput.
349    pub fn setup_create_contract(
350        &mut self,
351        contract: &Contract,
352    ) -> SHA256Hash {
353        let contract_bytes = txs::encode(&contract).unwrap();
354        let res = unsafe {
355            thruput_sys::setup_create_contract(
356                self.contract_context.context,
357                self.db.db,
358                contract_bytes.as_ptr(),
359                contract_bytes.len().try_into().unwrap(),
360            )
361        };
362        if res != 0 {
363            let err: thruput_sys::FFIErrorCode = res.into();
364            if err == thruput_sys::FFIErrorCode::ContractVerificationFailed {
365                panic!(
366                    "setup_create_contract failed: {err}\n\
367                     \n\
368                     The contract's rv32 artifact failed validation. Common causes:\n\
369                     - The raw ELF was deployed instead of the lowered .rv32a image\n\
370                     - Memory image bounds too large\n\
371                     - Unsupported instructions (only the RV32IM subset is accepted)\n"
372                );
373            }
374            panic!("setup_create_contract failed: {err}");
375        }
376        let mut hasher = Sha256::new();
377        hasher.update(&contract_bytes);
378        let res: [u8; 32] = hasher.finalize().into();
379        res.into()
380    }
381
382    /// Deploys the specified contract at the specified address.
383    /// The contract must already have been created, either by a transaction in an earlier block
384    /// or (in the test framework) by the setup_create_contract function.
385    ///
386    /// In the test framework, the semantics of the setup_{create,deploy}_contract functions
387    /// are that after the function is called, the effects on the database are as though that
388    /// contract had been {created,deployed} during the previous block.
389    ///
390    /// This address can be subsequently invoked.
391    ///
392    /// This contract will fail if the contract hash does not exist as an existing contract within Thruput
393    /// (i.e. if the contract was not created by the setup_create_contract function,
394    /// or was not created by a transaction in an earlier block).
395    pub fn setup_create_deployment(
396        &mut self,
397        addr: &ContractAddress,
398        contract: &Contract,
399    ) {
400        let contract_bytes = txs::encode(&contract).unwrap();
401
402        let mut hasher = Sha256::new();
403        hasher.update(&contract_bytes);
404        let res: [u8; 32] = hasher.finalize().into();
405        let contract_hash: SHA256Hash = res.into();
406
407        let contract_size = deployment_contract_size(contract);
408
409        let res = unsafe {
410            thruput_sys::setup_deploy_contract(
411                self.db.db,
412                addr.as_ptr(),
413                contract_hash.as_ptr(),
414                contract_size,
415            )
416        };
417        let err: FFIErrorCode = res.into();
418        if err != FFIErrorCode::None {
419            panic!("failed to setup new contract deployment: {:?}", err);
420        }
421    }
422
423    /// Grants the specified number of storage tokens to the specified contract address.
424    ///
425    /// In the test framework, the semantics of this function is that
426    /// after this function is called, the effects on the database are as though that
427    /// contract had been granted the specified number of storage tokens during the previous block.
428    ///
429    /// This function should not be called once the contract has been deployed and invoked;
430    /// the internal accounting of storage tokens will be incorrect if that occurs.
431    pub fn setup_grant_storage_tokens(
432        &mut self,
433        addr: &ContractAddress,
434        tokens: i64,
435    ) {
436        let res = unsafe {
437            thruput_sys::setup_grant_contract_storage_tokens(
438                self.db.db,
439                addr.as_ptr(),
440                tokens,
441            )
442        };
443        let err: FFIErrorCode = res.into();
444        if err != FFIErrorCode::None {
445            panic!("failed to setup grant storage tokens: {:?}", err);
446        }
447    }
448
449    /// Advances the test state to the next block.
450    /// This makes the effects of the previous block visible to the next block.
451    /// Only the transactions that were successfully committed will be visible to the next block.
452    /// An error may occur if there are any transactions that executed but did not commit or abort.
453    ///
454    /// The new block takes its timepoint from the wall clock. Use
455    /// [`TestEnvironmentInner::advance_block_at`] to pin one instead.
456    pub fn advance_block(&mut self) {
457        self.advance_block_inner(std::ptr::null())
458    }
459
460    /// Advances to the next block as [`TestEnvironmentInner::advance_block`]
461    /// does, but pins the new block's timepoint rather than reading the clock.
462    ///
463    /// Blocks only move forward in time: `block_timepoint` must be at or after
464    /// the open block's, and the host refuses an earlier one.
465    pub fn advance_block_at(&mut self, block_timepoint: TimePoint) {
466        let raw: u64 = block_timepoint.into_inner();
467        self.advance_block_inner(&raw)
468    }
469
470    fn advance_block_inner(&mut self, block_timepoint: *const u64) {
471        let res = unsafe {
472            thruput_sys::advance_new_block(self.db.db, block_timepoint)
473        };
474        let err: FFIErrorCode = res.into();
475        if err == FFIErrorCode::InvalidArgument {
476            panic!(
477                "failed to advance block: the requested timepoint precedes the \
478                 block it would follow. Blocks only move forward in time; pin a \
479                 timepoint at or after the current block's."
480            );
481        }
482        if err != FFIErrorCode::None {
483            panic!("failed to advance block: {:?}", err);
484        }
485    }
486
487    /// Registers a new mock contract with the test environment.
488    /// This does not create the contract as something visible within Thruput,
489    /// nor does it deploy it at a particular address.
490    ///
491    /// This contract (a Contract(ContractType::kMockTest))
492    /// can subsequently be created by the setup_create_contract functions,
493    /// or by being attached as one of the newly created contracts
494    /// on a SignedTransaction.
495    pub fn add_contract(&mut self) -> *mut MockContractInner {
496        let out: *mut MockContractInner = Box::into_raw(Box::new(
497            MockContractInner::new(self.mock_contract_index),
498        ));
499
500        let new_id = self.mock_contract_index;
501
502        self.contracts.push(out);
503        self.mock_contract_index += 1;
504
505        let res = unsafe {
506            thruput_sys::register_new_contract(
507                self.contract_context.context,
508                new_id,
509                Some(trampoline),
510                out as *mut c_void,
511            )
512        };
513        let err: FFIErrorCode = res.into();
514        if err != FFIErrorCode::None {
515            panic!("failed to link fn: {:?}", err);
516        }
517
518        out
519    }
520
521    /// The storage cost model the environment's database is running with.
522    pub fn get_current_cost_model(&self) -> StorageCostModel {
523        self.db.get_cost_model()
524    }
525
526    /// The execution parameters the environment's chain is running under.
527    pub fn get_network_config(&self) -> NetworkConfig {
528        self.db.get_network_config()
529    }
530
531    /// Verify `tx`'s auth claims; see
532    /// [`TestEnvironment::verify_signed_transaction_auth`].
533    pub fn run_auth_checks(
534        &self,
535        tx: &SignedTransaction,
536    ) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
537        let serialized_tx = txs::encode(tx).unwrap();
538        let ffi_serialized_tx = thruput_sys::FFISerializedSignedTransaction {
539            data: serialized_tx.as_ptr() as *mut u8,
540            len: serialized_tx.len().try_into().unwrap(),
541        };
542        let res = unsafe {
543            thruput_sys::verify_signed_transaction_auth_from_db(
544                self.db.db,
545                ffi_serialized_tx,
546            )
547        };
548        let err: FFIErrorCode = res.ffi_error_code.into();
549        if err != FFIErrorCode::None {
550            panic!("failed to run auth checks: {:?}", err);
551        }
552        res.auth_error_code.into()
553    }
554}
555
556impl Drop for TestEnvironmentInner {
557    fn drop(&mut self) {
558        for contract in &self.contracts {
559            let _ = unsafe { Box::from_raw(*contract) };
560        }
561    }
562}
563
564impl Default for TestEnvironmentInner {
565    fn default() -> Self {
566        Self::new(
567            None,
568            &test_network_hash(),
569            &mock_genesis_root_contract(),
570            None,
571        )
572    }
573}
574
575/// Shared handle to a [`TestEnvironment`]; returned by its constructors.
576pub type TestEnvironmentHandle = Rc<TestEnvironment>;
577
578/// The genesis root installed by the default constructors: a mock contract
579/// whose id sits at the top of the range, away from the low ids tests
580/// typically register. The test environment executes contract bodies
581/// natively through the mock trampoline, so the genesis root's code never
582/// runs — only its deployment exists. Use
583/// [`TestEnvironment::with_genesis_root_contract`] to install a different
584/// contract (e.g. a real rv32 artifact).
585pub fn mock_genesis_root_contract() -> Contract {
586    Contract::kMockTest {
587        mock_contract_id: u32::MAX as i32,
588    }
589}
590
591impl TestEnvironment {
592    /// Creates an environment on the default test network
593    /// ([`test_network_hash`]).
594    pub fn new(
595        cost_model: Option<&StorageCostModel>,
596    ) -> TestEnvironmentHandle {
597        Self::with_network_hash(cost_model, &test_network_hash())
598    }
599
600    /// Creates an environment on a caller-chosen network, with
601    /// [`mock_genesis_root_contract`] as the genesis root.
602    pub fn with_network_hash(
603        cost_model: Option<&StorageCostModel>,
604        network_hash: &NetworkHash,
605    ) -> TestEnvironmentHandle {
606        Self::with_genesis_root_contract(
607            cost_model,
608            network_hash,
609            &mock_genesis_root_contract(),
610        )
611    }
612
613    /// Creates an environment installing a caller-chosen genesis root
614    /// contract instead of [`mock_genesis_root_contract`].
615    pub fn with_genesis_root_contract(
616        cost_model: Option<&StorageCostModel>,
617        network_hash: &NetworkHash,
618        genesis_root_contract: &Contract,
619    ) -> TestEnvironmentHandle {
620        Self::from_parts(cost_model, network_hash, genesis_root_contract, None)
621    }
622
623    /// Start configuring an environment that needs settings the plain
624    /// constructors do not reach -- notably the chain's network config
625    /// parameters.
626    ///
627    /// Returns a [`TestEnvironmentBuilder`]; chain `with_*` methods, then
628    /// finish with [`build`](TestEnvironmentBuilder::build):
629    ///
630    /// ```no_run
631    /// use thruput_native_exec::test_environment::TestEnvironment;
632    ///
633    /// let env = TestEnvironment::builder()
634    ///     .no_cost()
635    ///     .with_max_sha256_buffers(2)
636    ///     .build();
637    /// ```
638    pub fn builder() -> TestEnvironmentBuilder {
639        TestEnvironmentBuilder::new()
640    }
641
642    /// Every constructor's common path: the one place an environment's
643    /// database and runtime context are created. `network_config` is
644    /// `None` for the stock execution parameters.
645    pub(crate) fn from_parts(
646        cost_model: Option<&StorageCostModel>,
647        network_hash: &NetworkHash,
648        genesis_root_contract: &Contract,
649        network_config: Option<&NetworkConfig>,
650    ) -> TestEnvironmentHandle {
651        let te = Rc::new(Self {
652            inner: RefCell::new(TestEnvironmentInner::new(
653                cost_model,
654                network_hash,
655                genesis_root_contract,
656                network_config,
657            )),
658        });
659
660        if std::env::var("SDK_TEST_ENABLE_SYSCALL_LOGGING").is_ok() {
661            Self::enable_syscall_logging();
662        }
663
664        te
665    }
666
667    /// Environment with the chain-default cost model on the default test
668    /// network. Not the `Default` trait: constructors return a handle.
669    #[allow(clippy::should_implement_trait)]
670    pub fn default() -> TestEnvironmentHandle {
671        Self::new(None)
672    }
673
674    /// Environment in which every storage operation costs zero.
675    pub fn no_cost() -> TestEnvironmentHandle {
676        Self::new(Some(
677            &thruput_storage_cost::StorageCostModel::no_cost_model(),
678        ))
679    }
680
681    /// Turn on FFI-level syscall logging to stderr for environments
682    /// created after the call.
683    pub fn enable_syscall_logging() {
684        TestEnvironmentInner::enable_syscall_logging(
685            thruput_sys::FFILogLevel::Trace,
686        );
687    }
688
689    /// Advance to the next block, making committed effects visible to
690    /// subsequent transactions. Panics if a transaction executed but was
691    /// neither committed nor aborted.
692    ///
693    /// The new block takes its timepoint from the wall clock, so
694    /// `thruput_sdk::block::get_block_timepoint` reports a plausible
695    /// present-day value that moves between blocks. Use `advance_block_at` to
696    /// drive contract logic keyed on time -- deadlines, expiry, vesting, rate
697    /// limits.
698    ///
699    /// The SDK path is a plain code span, not an intra-doc link:
700    /// `thruput-sdk` depends on this crate, not the other way round, so there
701    /// is nothing here to resolve it against.
702    pub fn advance_block(self: &TestEnvironmentHandle) {
703        self.inner.borrow_mut().advance_block()
704    }
705
706    /// Advance to the next block with its timepoint pinned to
707    /// `block_timepoint` (Unix nanoseconds) rather than read from the clock.
708    ///
709    /// Blocks only move forward in time, so this panics if `block_timepoint`
710    /// precedes the current block's. Note the environment opens its first
711    /// block on the wall clock, so pin forward from the current
712    /// `thruput_sdk::block::get_block_timepoint` rather than at an absolute
713    /// past date.
714    pub fn advance_block_at(
715        self: &TestEnvironmentHandle,
716        block_timepoint: TimePoint,
717    ) {
718        self.inner.borrow_mut().advance_block_at(block_timepoint)
719    }
720
721    /// The storage cost model the environment's database is running with.
722    pub fn get_current_cost_model(
723        self: &TestEnvironmentHandle,
724    ) -> StorageCostModel {
725        self.inner.borrow().get_current_cost_model()
726    }
727
728    /// The execution parameters the environment's chain is running under
729    /// (e.g. `max_sha256_buffers`, the VM's live hash-stream cap).
730    pub fn get_network_config(self: &TestEnvironmentHandle) -> NetworkConfig {
731        self.inner.borrow().get_network_config()
732    }
733
734    /// Read a committed contract-data item from the current block snapshot
735    /// (`None` if the key holds nothing).
736    pub fn get_snapshot_storage_item(
737        self: &TestEnvironmentHandle,
738        addr: &ContractAddress,
739        key: &StorageKeyContractData,
740    ) -> Option<WireStorageItem> {
741        self.inner.borrow().db.get_snapshot_storage_item(addr, key)
742    }
743
744    /// Read an address's committed [`AddressAuthData`] (its registered keys
745    /// and required weight) from the current block snapshot (`None` if the
746    /// address has none).
747    pub fn get_snapshot_auth_data(
748        self: &TestEnvironmentHandle,
749        addr: &ContractAddress,
750    ) -> Option<AddressAuthData> {
751        self.inner.borrow().db.get_snapshot_auth_data(addr)
752    }
753
754    /// Internal: create an empty mock contract. Use `register_mock` (or the
755    /// generated `register_mock_contract`) instead.
756    #[doc(hidden)]
757    pub fn add_contract(self: &TestEnvironmentHandle) -> MockContract {
758        let inner = self.inner.borrow_mut().add_contract();
759        MockContract::new(inner, self.clone())
760    }
761
762    /// Create a mock contract, register methods onto it, and make it
763    /// deployable in the test environment.
764    ///
765    /// The registrar may be a single generated `__mock` function or a
766    /// closure that combines multiple interface registrars onto one mock.
767    pub fn register_mock<F>(self: &TestEnvironmentHandle, f: F) -> MockContract
768    where
769        F: FnOnce(&mut MockContract),
770    {
771        let mut c = self.add_contract();
772        f(&mut c);
773        self.setup_create_contract(&c.as_contract());
774        c
775    }
776
777    /// Start configuring a transaction to execute in this environment.
778    ///
779    /// Returns a [`TransactionBuilder`]; chain `with_*` methods (e.g.
780    /// [`with_auth_claim`](TransactionBuilder::with_auth_claim)) to customize
781    /// the mocked transaction context, then finish with
782    /// [`transact`](TransactionBuilder::transact),
783    /// [`transact_manual`](TransactionBuilder::transact_manual), or
784    /// [`build`](TransactionBuilder::build).
785    ///
786    /// ```ignore
787    /// env.transaction()
788    ///     .with_auth_claim(my_claim)
789    ///     .transact(|tx| tx.call(&addr).run(|| { /* ... */ }));
790    /// ```
791    pub fn transaction(self: &TestEnvironmentHandle) -> TransactionBuilder {
792        TransactionBuilder::new(self.clone())
793    }
794
795    /// Execute a transaction with auto-commit and the default configuration.
796    ///
797    /// Creates a MockTransactionHandler, gives the closure a mutable reference,
798    /// and always calls try_commit_transaction when the closure returns
799    /// (hogd handles abort internally when the root frame reverted).
800    ///
801    /// The transaction has exactly one root frame: a second `tx.call`
802    /// inside the closure panics; use a child frame or another `transact`.
803    ///
804    /// Shorthand for `env.transaction().transact(f)`; use
805    /// [`transaction`](Self::transaction) to customize the transaction first.
806    #[must_use]
807    pub fn transact(
808        self: &TestEnvironmentHandle,
809        f: impl FnOnce(&mut MockTransactionHandler) -> FrameResult,
810    ) -> TransactResult {
811        self.transaction().transact(f)
812    }
813
814    /// Execute a transaction with manual commit/abort control and the default
815    /// configuration.
816    ///
817    /// The closure receives a MockTransactionHandler and is responsible for
818    /// calling tx.try_commit_transaction() or tx.abort_transaction().
819    /// The closure's return value is forwarded as the return value of transact_manual.
820    ///
821    /// Shorthand for `env.transaction().transact_manual(f)`; use
822    /// [`transaction`](Self::transaction) to customize the transaction first.
823    pub fn transact_manual<R>(
824        self: &TestEnvironmentHandle,
825        f: impl FnOnce(&mut MockTransactionHandler) -> R,
826    ) -> R {
827        self.transaction().transact_manual(f)
828    }
829
830    // -- Setup-phase methods --
831    //
832    // The setup_* methods seed Thruput state directly, bypassing
833    // transaction execution. Call them before running any transactions in
834    // a test; their effects appear as if applied in a previous block.
835
836    /// Setup phase: create a contract in Thruput state (not deployed at
837    /// any address). Returns the contract hash. Run before any
838    /// transactions in the test.
839    pub fn setup_create_contract(
840        self: &TestEnvironmentHandle,
841        contract: &Contract,
842    ) -> SHA256Hash {
843        self.inner.borrow_mut().setup_create_contract(contract)
844    }
845
846    /// Setup phase: deploy an already-created contract at `addr`. Run
847    /// before any transactions in the test.
848    pub fn setup_create_deployment(
849        self: &TestEnvironmentHandle,
850        addr: &ContractAddress,
851        contract: &Contract,
852    ) {
853        self.inner
854            .borrow_mut()
855            .setup_create_deployment(addr, contract)
856    }
857
858    /// Setup phase: grant storage tokens to the contract at `addr`, as if
859    /// granted in a previous block. Like `setup_create_contract` and
860    /// `setup_create_deployment`, run before any transactions in the test.
861    pub fn setup_grant_storage_tokens(
862        self: &TestEnvironmentHandle,
863        addr: &ContractAddress,
864        tokens: i64,
865    ) {
866        self.inner
867            .borrow_mut()
868            .setup_grant_storage_tokens(addr, tokens)
869    }
870
871    /// Check the auth claims on the provided transaction
872    /// against the auth data loaded from the database.
873    ///
874    /// May perform additional checks (such as checking block height limits)
875    /// not performed by the static version.  Will additionally fail if
876    /// addresses without auth data are referenced.
877    pub fn verify_signed_transaction_auth(
878        self: &TestEnvironmentHandle,
879        tx: &SignedTransaction,
880    ) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
881        self.inner.borrow().run_auth_checks(tx)
882    }
883
884    /// Check the auth claims on the provided transaction
885    /// against the provided source and auxiliary auth data.
886    /// Does not load any information from the database.
887    ///
888    /// Pass `None` to represent "no registered auth data" (the pubkey-claim
889    /// case). Pass `Some(d)` to supply serialized [`AddressAuthData`].
890    ///
891    /// `network_hash` is part of every signature preimage, so it must be the
892    /// network the signatures were produced for — see
893    /// `thruput_domain_separator::transaction_signature_preimage`. There is
894    /// no database here to read it from, so it is passed explicitly;
895    /// [`test_network_hash`] is what a [`TestEnvironment`] would use.
896    pub fn verify_signed_transaction_auth_static(
897        tx: &SignedTransaction,
898        source_auth_data: Option<&AddressAuthData>,
899        network_hash: &NetworkHash,
900    ) -> thruput_xdr::auth_claim::AuthVerifyErrorCode {
901        let serialized_tx = txs::encode(tx).unwrap();
902        let ffi_serialized_tx = thruput_sys::FFISerializedSignedTransaction {
903            data: serialized_tx.as_ptr() as *mut u8,
904            len: serialized_tx.len().try_into().unwrap(),
905        };
906
907        // `None` → null pointer with len=0 (pubkey-claim path, 0.20.1+).
908        // `Some(d)` → serialize and pass as before.
909        let serialized_source_auth_data =
910            source_auth_data.map(|d| txs::encode(d).unwrap());
911        let ffi_serialized_source_auth_data =
912            match &serialized_source_auth_data {
913                Some(bytes) => thruput_sys::FFISourceAuthData {
914                    data: bytes.as_ptr() as *mut u8,
915                    len: bytes.len().try_into().unwrap(),
916                },
917                None => thruput_sys::FFISourceAuthData {
918                    data: core::ptr::null_mut(),
919                    len: 0,
920                },
921            };
922
923        let res = unsafe {
924            thruput_sys::verify_signed_transaction_auth(
925                ffi_serialized_tx,
926                ffi_serialized_source_auth_data,
927                network_hash.as_ptr(),
928            )
929        };
930        let err: FFIErrorCode = res.ffi_error_code.into();
931        if err != FFIErrorCode::None {
932            panic!("failed to run auth checks: {:?}", err);
933        }
934        res.auth_error_code.into()
935    }
936}
937
938fn deployment_contract_size(contract: &Contract) -> u32 {
939    match contract {
940        Contract::kNative { .. } => 0,
941        Contract::kMockTest { .. } => TEST_CONTRACT_SIZE,
942        Contract::kRv32 { rv32_contract } => rv32_contract.data.len() as u32,
943        other => panic!("unsupported contract variant: {:?}", other),
944    }
945}
946
947#[cfg(test)]
948mod tests {
949    use super::*;
950    use xdrs::BytesM;
951
952    const TEST_ADDR: ContractAddress = ContractAddress::new([0x11; 32]);
953
954    #[test]
955    fn test_network_hash_is_sha256_of_the_test_network_name() {
956        // Pinned so the network every test runs on cannot drift silently:
957        // sha256("LOCAL_UNIT_TEST").
958        let expected: [u8; 32] = [
959            0xb0, 0xb2, 0xfc, 0xc8, 0x57, 0xda, 0xa6, 0x04, 0x78, 0x7b, 0xb8,
960            0x4b, 0x45, 0xa4, 0xca, 0xf0, 0x32, 0xea, 0x6b, 0x93, 0xf5, 0xa5,
961            0xed, 0x1c, 0x14, 0xa9, 0xbf, 0xf8, 0xfa, 0x65, 0xbb, 0x52,
962        ];
963        assert_eq!(test_network_hash(), NetworkHash::new(expected.into()));
964    }
965
966    #[test]
967    fn deployment_contract_size_uses_rv32_blob_length() {
968        let contract = Contract::kRv32 {
969            rv32_contract: ContractBlob {
970                data: BytesM(vec![1, 2, 3, 4, 5]),
971            },
972        };
973
974        assert_eq!(deployment_contract_size(&contract), 5);
975    }
976
977    #[test]
978    fn get_snapshot_storage_item_reads_committed_bytestring() {
979        let env = TestEnvironment::new(None);
980        let key = StorageKeyContractData::new([0x22; 32]);
981        env.setup_grant_storage_tokens(&TEST_ADDR, 1_000_000_000);
982
983        let result = env.transact(|tx| {
984            tx.call(&TEST_ADDR).run(|| {
985                thruput_xdr::syscalls::raw_BytestringSet(&key, b"hello")
986                    .unwrap();
987            })
988        });
989        result.assert_success();
990
991        env.advance_block();
992
993        let item = env.get_snapshot_storage_item(&TEST_ADDR, &key);
994        assert_eq!(
995            item,
996            Some(WireStorageItem::kItemTypeBytestring {
997                bytestring: b"hello".to_vec().try_into().unwrap(),
998            }),
999        );
1000    }
1001
1002    #[test]
1003    fn get_snapshot_storage_item_returns_none_for_missing_key() {
1004        let env = TestEnvironment::new(None);
1005        let key = StorageKeyContractData::new([0x33; 32]);
1006
1007        assert_eq!(env.get_snapshot_storage_item(&TEST_ADDR, &key), None);
1008    }
1009
1010    #[test]
1011    fn get_snapshot_storage_item_reads_committed_bytestring_on_deployed_address()
1012     {
1013        let env = TestEnvironment::new(None);
1014        let key = StorageKeyContractData::new([0x44; 32]);
1015        env.setup_grant_storage_tokens(&TEST_ADDR, 1_000_000_000);
1016        // Any created, deployable contract works: the transact below runs
1017        // through the mock trampoline regardless of the deployed type, and
1018        // native precompiles are banned from redeployment.
1019        // A fresh environment hands out mock contract ids from zero.
1020        let _mock = env.inner.borrow_mut().add_contract();
1021        let contract = thruput_xdr::contract::Contract::kMockTest {
1022            mock_contract_id: 0,
1023        };
1024        env.inner.borrow_mut().setup_create_contract(&contract);
1025        env.inner
1026            .borrow_mut()
1027            .setup_create_deployment(&TEST_ADDR, &contract);
1028
1029        let result = env.transact(|tx| {
1030            tx.call(&TEST_ADDR).run(|| {
1031                thruput_xdr::syscalls::raw_BytestringSet(&key, b"hello")
1032                    .unwrap();
1033            })
1034        });
1035        result.assert_success();
1036
1037        env.advance_block();
1038
1039        let item = env.get_snapshot_storage_item(&TEST_ADDR, &key);
1040        assert_eq!(
1041            item,
1042            Some(WireStorageItem::kItemTypeBytestring {
1043                bytestring: b"hello".to_vec().try_into().unwrap(),
1044            }),
1045        );
1046    }
1047}