Skip to main content

ab_executor_native/
lib.rs

1#![feature(const_convert, const_trait_impl, slice_ptr_get)]
2
3mod context;
4
5use crate::context::{MethodDetails, NativeExecutorContext};
6use ab_aligned_buffer::SharedAlignedBuffer;
7use ab_contracts_common::env::{Env, EnvState, MethodContext};
8use ab_contracts_common::metadata::decode::{MetadataDecoder, MetadataDecodingError, MetadataItem};
9use ab_contracts_common::method::MethodFingerprint;
10use ab_contracts_common::{
11    Contract, ContractError, ContractTrait, ContractTraitDefinition, MAX_CODE_SIZE,
12    NativeExecutorContactMethod,
13};
14use ab_contracts_standards::fungible::Fungible;
15use ab_contracts_standards::tx_handler::TxHandlerExt;
16use ab_core_primitives::address::Address;
17use ab_core_primitives::balance::Balance;
18use ab_core_primitives::shard::ShardIndex;
19use ab_core_primitives::transaction::{Transaction, TransactionHeader, TransactionSlot};
20use ab_executor_slots::{Slot, SlotKey, Slots};
21use ab_io_type::variable_bytes::VariableBytes;
22use ab_io_type::variable_elements::VariableElements;
23use ab_system_contract_address_allocator::{AddressAllocator, AddressAllocatorExt};
24use ab_system_contract_block::{Block, BlockExt};
25use ab_system_contract_code::{Code, CodeExt};
26use ab_system_contract_native_token::{NativeToken, NativeTokenExt};
27use ab_system_contract_simple_wallet_base::SimpleWalletBase;
28use ab_system_contract_state::State;
29use halfbrown::HashMap;
30
31/// Native executor errors
32#[derive(Debug, thiserror::Error)]
33pub enum NativeExecutorError {
34    /// Contract metadata not found
35    #[error("Contract metadata not found")]
36    ContractMetadataNotFound,
37    /// Contract metadata decoding error
38    #[error("Contract metadata decoding error: {error}")]
39    ContractMetadataDecodingError {
40        error: MetadataDecodingError<'static>,
41    },
42    /// Expected contract metadata, found trait
43    #[error("Expected contract metadata, found trait")]
44    ExpectedContractMetadataFoundTrait,
45    /// Duplicate method in contract
46    #[error("Duplicate method fingerprint {method_fingerprint} for contract code {contact_code}")]
47    DuplicateMethodInContract {
48        /// Name of the crate in which the method was duplicated
49        contact_code: &'static str,
50        /// Method fingerprint
51        method_fingerprint: &'static MethodFingerprint,
52    },
53}
54
55#[derive(Debug, Clone)]
56struct MethodsEntry {
57    contact_code: &'static str,
58    main_contract_metadata: &'static [u8],
59    native_executor_methods: &'static [NativeExecutorContactMethod],
60}
61
62/// Builder for [`NativeExecutor`]
63#[derive(Debug, Clone)]
64pub struct NativeExecutorBuilder {
65    shard_index: ShardIndex,
66    methods: Vec<MethodsEntry>,
67}
68
69impl NativeExecutorBuilder {
70    fn new(shard_index: ShardIndex) -> Self {
71        let instance = Self {
72            shard_index,
73            methods: Vec::new(),
74        };
75
76        // Start with system contracts
77        instance
78            .with_contract::<AddressAllocator>()
79            .with_contract::<Block>()
80            .with_contract::<Code>()
81            .with_contract::<NativeToken>()
82            .with_contract_trait::<NativeToken, dyn Fungible>()
83            .with_contract::<SimpleWalletBase>()
84            .with_contract::<State>()
85    }
86
87    /// Make the native execution environment aware of the contract specified in the generic
88    /// argument.
89    ///
90    /// Here `C` is the contract type:
91    /// ```ignore
92    /// # fn foo(mut builder: NativeExecutorBuilder) -> NativeExecutorBuilder {
93    ///     builder.with_contract::<Flipper>()
94    /// # }
95    /// ```
96    ///
97    /// NOTE: System contracts are already included by default.
98    #[must_use]
99    pub fn with_contract<C>(mut self) -> Self
100    where
101        C: Contract,
102    {
103        self.methods.push(MethodsEntry {
104            contact_code: C::CODE,
105            main_contract_metadata: C::MAIN_CONTRACT_METADATA,
106            native_executor_methods: C::NATIVE_EXECUTOR_METHODS,
107        });
108        self
109    }
110
111    /// Make the native execution environment aware of the trait implemented by the contract
112    /// specified in the generic argument.
113    ///
114    /// Here `C` is the contract type and `DynCT` is a trait it implements in the form of
115    /// `dyn ContractTrait`:
116    /// ```ignore
117    /// # fn foo(mut builder: NativeExecutorBuilder) -> NativeExecutorBuilder {
118    ///     builder
119    ///         .with_contract::<Token>()
120    ///         .with_contract_trait::<Token, dyn Fungible>()
121    /// # }
122    /// ```
123    #[must_use]
124    pub fn with_contract_trait<C, DynCT>(mut self) -> Self
125    where
126        C: Contract + ContractTrait<DynCT>,
127        DynCT: ContractTraitDefinition + ?Sized,
128    {
129        self.methods.push(MethodsEntry {
130            contact_code: C::CODE,
131            main_contract_metadata: C::MAIN_CONTRACT_METADATA,
132            native_executor_methods: <C as ContractTrait<DynCT>>::NATIVE_EXECUTOR_METHODS,
133        });
134        self
135    }
136
137    /// Build native execution configuration
138    pub fn build(self) -> Result<NativeExecutor, NativeExecutorError> {
139        // 10 is a decent capacity for many typical cases without reallocation
140        let mut methods_by_code = HashMap::with_capacity(10);
141        for methods_entry in self.methods {
142            let MethodsEntry {
143                contact_code,
144                main_contract_metadata,
145                native_executor_methods,
146            } = methods_entry;
147            for &native_executor_method in native_executor_methods {
148                let NativeExecutorContactMethod {
149                    method_fingerprint,
150                    method_metadata,
151                    ffi_fn,
152                } = native_executor_method;
153                #[expect(
154                    clippy::rest_pattern_accessible_field,
155                    reason = "Do not need other fields"
156                )]
157                let recommended_capacities = match MetadataDecoder::new(main_contract_metadata)
158                    .decode_next()
159                    .ok_or(NativeExecutorError::ContractMetadataNotFound)?
160                    .map_err(|error| NativeExecutorError::ContractMetadataDecodingError { error })?
161                {
162                    MetadataItem::Contract {
163                        state_type_details,
164                        slot_type_details,
165                        tmp_type_details,
166                        ..
167                    } => (
168                        state_type_details.recommended_capacity,
169                        slot_type_details.recommended_capacity,
170                        tmp_type_details.recommended_capacity,
171                    ),
172                    MetadataItem::Trait { .. } => {
173                        return Err(NativeExecutorError::ExpectedContractMetadataFoundTrait);
174                    }
175                };
176                let (
177                    recommended_state_capacity,
178                    recommended_slot_capacity,
179                    recommended_tmp_capacity,
180                ) = recommended_capacities;
181
182                if methods_by_code
183                    .insert(
184                        (contact_code.as_bytes(), method_fingerprint),
185                        MethodDetails {
186                            recommended_state_capacity,
187                            recommended_slot_capacity,
188                            recommended_tmp_capacity,
189                            method_metadata,
190                            ffi_fn,
191                        },
192                    )
193                    .is_some()
194                {
195                    return Err(NativeExecutorError::DuplicateMethodInContract {
196                        contact_code,
197                        method_fingerprint,
198                    });
199                }
200            }
201        }
202
203        Ok(NativeExecutor {
204            shard_index: self.shard_index,
205            methods_by_code,
206        })
207    }
208}
209
210/// Executor of contracts compiled natively for the host.
211///
212/// It trusts contracts and data they forward (like transaction payloads): methods are called
213/// through pointers to arguments as provided by callers, without checking them against method
214/// metadata, which keeps execution as cheap as possible. Arguments whose sizes, capacities or
215/// alignment don't match metadata of the called method are undefined behavior rather than an
216/// error, which running tests under Miri can detect. For `#[input]` and `#[output]` arguments,
217/// debug assertions of the called method also catch such mismatches in debug builds. Contents of
218/// `#[input]` and `#[output]` arguments are parsed by the called method itself, which returns
219/// `ContractError::BadInput` for invalid ones. Executors of untrusted contracts must check sizes,
220/// capacities and alignment of arguments against method metadata, like the RISC-V VM does.
221#[derive(Debug)]
222pub struct NativeExecutor {
223    shard_index: ShardIndex,
224    /// Indexed by contract's code and method fingerprint
225    methods_by_code: HashMap<(&'static [u8], &'static MethodFingerprint), MethodDetails>,
226}
227
228impl NativeExecutor {
229    /// Create a new [`Slots`] instance with system contracts already deployed
230    pub fn new_storage_slots(&self) -> Result<Slots, ContractError> {
231        // Manually deploy code of system code contract
232        let slots = [Slot::ReadWrite {
233            key: SlotKey {
234                owner: Address::SYSTEM_CODE,
235                contract: Address::SYSTEM_CODE,
236            },
237            buffer: SharedAlignedBuffer::from_bytes(Code::code().get_initialized()),
238        }];
239
240        let address_allocator_address = Address::system_address_allocator(self.shard_index);
241        let mut slots = Slots::new(slots);
242
243        let system_contracts: [(Address, &VariableBytes<'_, { MAX_CODE_SIZE }>); _] = [
244            (Address::SYSTEM_STATE, &State::code()),
245            (address_allocator_address, &AddressAllocator::code()),
246            (Address::SYSTEM_BLOCK, &Block::code()),
247            (Address::SYSTEM_NATIVE_TOKEN, &NativeToken::code()),
248            (
249                Address::SYSTEM_SIMPLE_WALLET_BASE,
250                &SimpleWalletBase::code(),
251            ),
252        ];
253
254        {
255            let mut nested_slots = slots.new_nested_rw();
256            // Allow deployment of system contracts
257            for (address, _code) in system_contracts {
258                let result = nested_slots.add_new_contract(address);
259                debug_assert!(result);
260            }
261            nested_slots.commit();
262        }
263
264        // Deploy and initialize other system contacts
265        self.transaction_emulate(Address::NULL, &mut slots, |env| {
266            for (address, code) in system_contracts {
267                env.code_store(MethodContext::Reset, Address::SYSTEM_CODE, &address, code)?;
268            }
269
270            env.address_allocator_new(MethodContext::Reset, address_allocator_address)?;
271            env.block_genesis(MethodContext::Reset, Address::SYSTEM_BLOCK)?;
272            env.native_token_initialize(
273                MethodContext::Reset,
274                Address::SYSTEM_NATIVE_TOKEN,
275                &Address::SYSTEM_NATIVE_TOKEN,
276                // TODO: The balance should not be `max` by default, define rules and use them
277                &Balance::MAX,
278            )
279        })?;
280
281        Ok(slots)
282    }
283
284    /// Builder of native executor for specified shard index
285    #[must_use]
286    pub fn builder(shard_index: ShardIndex) -> NativeExecutorBuilder {
287        NativeExecutorBuilder::new(shard_index)
288    }
289
290    /// Verify the provided transaction.
291    ///
292    /// [`Self::transaction_execute()`] can be used for transaction execution if needed.
293    /// [`Self::transaction_verify_execute()`] can be used to verify and execute a transaction with
294    /// a single call, primarily for testing purposes.
295    pub fn transaction_verify(
296        &self,
297        transaction: Transaction<'_>,
298        slots: &Slots,
299    ) -> Result<(), ContractError> {
300        if transaction.header.version != TransactionHeader::TRANSACTION_VERSION {
301            return Err(ContractError::BadInput);
302        }
303
304        let env_state = EnvState {
305            shard_index: self.shard_index,
306            padding_0: [0; _],
307            own_address: Address::NULL,
308            context: Address::NULL,
309            caller: Address::NULL,
310        };
311
312        let read_slots_size =
313            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.read_slots))
314                .map_err(|_error| ContractError::BadInput)?;
315        let read_slots = VariableElements::from_buffer(transaction.read_slots, &read_slots_size)
316            .expect("Size is computed from the buffer above; qed");
317
318        let write_slots_size =
319            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.write_slots))
320                .map_err(|_error| ContractError::BadInput)?;
321        let write_slots = VariableElements::from_buffer(transaction.write_slots, &write_slots_size)
322            .expect("Size is computed from the buffer above; qed");
323
324        let payload_size = u32::try_from(size_of_val::<[u128]>(transaction.payload))
325            .map_err(|_error| ContractError::BadInput)?;
326        let payload = VariableElements::from_buffer(transaction.payload, &payload_size)
327            .expect("Size is computed from the buffer above; qed");
328
329        let seal_size = u32::try_from(size_of_val::<[u8]>(transaction.seal))
330            .map_err(|_error| ContractError::BadInput)?;
331        let seal = VariableBytes::from_buffer(transaction.seal, &seal_size)
332            .expect("Size is computed from the buffer above; qed");
333
334        let executor_context = NativeExecutorContext::new(
335            self.shard_index,
336            &self.methods_by_code,
337            slots.new_nested_ro(),
338            false,
339        );
340        // SAFETY: Created outside of contract code and given by reference only to an extension
341        // method, which borrows no memory for the lifetime of `Env`, and only dropped afterward
342        let env = unsafe { Env::with_executor_context(env_state, &executor_context) };
343        env.tx_handler_authorize(
344            transaction.header.contract,
345            transaction.header,
346            &read_slots,
347            &write_slots,
348            &payload,
349            &seal,
350        )
351    }
352
353    /// Execute the previously verified transaction.
354    ///
355    /// [`Self::transaction_verify()`] must be used for verification.
356    /// [`Self::transaction_verify_execute()`] can be used to verify and execute a transaction with
357    /// a single call, primarily for testing purposes.
358    pub fn transaction_execute(
359        &self,
360        transaction: Transaction<'_>,
361        slots: &mut Slots,
362    ) -> Result<(), ContractError> {
363        if transaction.header.version != TransactionHeader::TRANSACTION_VERSION {
364            return Err(ContractError::BadInput);
365        }
366
367        // TODO: This is a pretty large data structure to copy around, try to make it a reference
368        let env_state = EnvState {
369            shard_index: self.shard_index,
370            padding_0: [0; _],
371            own_address: Address::NULL,
372            context: Address::NULL,
373            caller: Address::NULL,
374        };
375
376        let read_slots_size =
377            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.read_slots))
378                .map_err(|_error| ContractError::BadInput)?;
379        let read_slots = VariableElements::from_buffer(transaction.read_slots, &read_slots_size)
380            .expect("Size is computed from the buffer above; qed");
381
382        let write_slots_size =
383            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.write_slots))
384                .map_err(|_error| ContractError::BadInput)?;
385        let write_slots = VariableElements::from_buffer(transaction.write_slots, &write_slots_size)
386            .expect("Size is computed from the buffer above; qed");
387
388        let payload_size = u32::try_from(size_of_val::<[u128]>(transaction.payload))
389            .map_err(|_error| ContractError::BadInput)?;
390        let payload = VariableElements::from_buffer(transaction.payload, &payload_size)
391            .expect("Size is computed from the buffer above; qed");
392
393        let seal_size = u32::try_from(size_of_val::<[u8]>(transaction.seal))
394            .map_err(|_error| ContractError::BadInput)?;
395        let seal = VariableBytes::from_buffer(transaction.seal, &seal_size)
396            .expect("Size is computed from the buffer above; qed");
397
398        let executor_context = NativeExecutorContext::new(
399            self.shard_index,
400            &self.methods_by_code,
401            slots.new_nested_rw(),
402            true,
403        );
404
405        // SAFETY: Created outside of contract code and given by reference only to an extension
406        // method, which borrows no memory for the lifetime of `Env`, and only dropped afterward
407        let mut env = unsafe { Env::with_executor_context(env_state, &executor_context) };
408        env.tx_handler_execute(
409            MethodContext::Reset,
410            transaction.header.contract,
411            transaction.header,
412            &read_slots,
413            &write_slots,
414            &payload,
415            &seal,
416        )?;
417
418        executor_context.into_slots().commit();
419
420        Ok(())
421    }
422
423    /// Verify and execute the provided transaction, primarily for testing purposes.
424    ///
425    /// A slightly more efficient shortcut for [`Self::transaction_verify()`] +
426    /// [`Self::transaction_execute()`] compared to calling them separately.
427    pub fn transaction_verify_execute(
428        &self,
429        transaction: Transaction<'_>,
430        slots: &mut Slots,
431    ) -> Result<(), ContractError> {
432        if transaction.header.version != TransactionHeader::TRANSACTION_VERSION {
433            return Err(ContractError::BadInput);
434        }
435
436        // TODO: This is a pretty large data structure to copy around, try to make it a reference
437        let env_state = EnvState {
438            shard_index: self.shard_index,
439            padding_0: [0; _],
440            own_address: Address::NULL,
441            context: Address::NULL,
442            caller: Address::NULL,
443        };
444
445        let read_slots_size =
446            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.read_slots))
447                .map_err(|_error| ContractError::BadInput)?;
448        let read_slots = VariableElements::from_buffer(transaction.read_slots, &read_slots_size)
449            .expect("Size is computed from the buffer above; qed");
450
451        let write_slots_size =
452            u32::try_from(size_of_val::<[TransactionSlot]>(transaction.write_slots))
453                .map_err(|_error| ContractError::BadInput)?;
454        let write_slots = VariableElements::from_buffer(transaction.write_slots, &write_slots_size)
455            .expect("Size is computed from the buffer above; qed");
456
457        let payload_size = u32::try_from(size_of_val::<[u128]>(transaction.payload))
458            .map_err(|_error| ContractError::BadInput)?;
459        let payload = VariableElements::from_buffer(transaction.payload, &payload_size)
460            .expect("Size is computed from the buffer above; qed");
461
462        let seal_size = u32::try_from(size_of_val::<[u8]>(transaction.seal))
463            .map_err(|_error| ContractError::BadInput)?;
464        let seal = VariableBytes::from_buffer(transaction.seal, &seal_size)
465            .expect("Size is computed from the buffer above; qed");
466
467        // TODO: Make it more efficient by not recreating NativeExecutorContext twice here
468        {
469            let executor_context = NativeExecutorContext::new(
470                self.shard_index,
471                &self.methods_by_code,
472                slots.new_nested_ro(),
473                false,
474            );
475            // SAFETY: Created outside of contract code and given by reference only to an extension
476            // method, which borrows no memory for the lifetime of `Env`, and only dropped afterward
477            let env = unsafe { Env::with_executor_context(env_state, &executor_context) };
478            env.tx_handler_authorize(
479                transaction.header.contract,
480                transaction.header,
481                &read_slots,
482                &write_slots,
483                &payload,
484                &seal,
485            )?;
486        }
487
488        {
489            let executor_context = NativeExecutorContext::new(
490                self.shard_index,
491                &self.methods_by_code,
492                slots.new_nested_rw(),
493                true,
494            );
495            // SAFETY: Created outside of contract code and given by reference only to an extension
496            // method, which borrows no memory for the lifetime of `Env`, and only dropped afterward
497            let mut env = unsafe { Env::with_executor_context(env_state, &executor_context) };
498            env.tx_handler_execute(
499                MethodContext::Reset,
500                transaction.header.contract,
501                transaction.header,
502                &read_slots,
503                &write_slots,
504                &payload,
505                &seal,
506            )?;
507
508            executor_context.into_slots().commit();
509        }
510
511        Ok(())
512    }
513
514    /// Emulate a transaction submitted by `contract` with method calls happening inside `calls`
515    /// without going through `TxHandler`.
516    ///
517    /// NOTE: This is primarily useful for testing environment, usually changes are done in the
518    /// transaction execution using [`Self::transaction_execute()`].
519    ///
520    /// Returns `None` if the read-only [`Slots`] instance was given.
521    pub fn transaction_emulate<Calls, T>(
522        &self,
523        contract: Address,
524        slots: &mut Slots,
525        calls: Calls,
526    ) -> T
527    where
528        Calls: FnOnce(&mut Env<'_>) -> T,
529    {
530        let env_state = EnvState {
531            shard_index: self.shard_index,
532            padding_0: [0; _],
533            own_address: contract,
534            context: contract,
535            caller: Address::NULL,
536        };
537
538        let executor_context = NativeExecutorContext::new(
539            self.shard_index,
540            &self.methods_by_code,
541            slots.new_nested_rw(),
542            true,
543        );
544        // SAFETY: Created outside of contract code and given by reference only to a callback that
545        // is generic over the lifetime of `Env`, so it can only borrow `'static` memory for that
546        // lifetime, and only dropped afterward
547        let mut env = unsafe { Env::with_executor_context(env_state, &executor_context) };
548        let result = calls(&mut env);
549
550        // Failed calls already rolled back their own changes, while changes of successful ones are
551        // kept regardless of what `calls` returns
552        executor_context.into_slots().commit();
553
554        result
555    }
556
557    /// Get a read-only `Env` instance for calling `#[view]` methods on it directly.
558    ///
559    /// For stateful methods, execute a transaction using [`Self::transaction_execute()`] or
560    /// emulate one with [`Self::transaction_emulate()`].
561    #[must_use]
562    pub fn with_env_ro<Callback, T>(&self, slots: &Slots, callback: Callback) -> T
563    where
564        Callback: FnOnce(&Env<'_>) -> T,
565    {
566        let env_state = EnvState {
567            shard_index: self.shard_index,
568            padding_0: [0; _],
569            own_address: Address::NULL,
570            context: Address::NULL,
571            caller: Address::NULL,
572        };
573
574        let executor_context = NativeExecutorContext::new(
575            self.shard_index,
576            &self.methods_by_code,
577            slots.new_nested_ro(),
578            false,
579        );
580        // SAFETY: Created outside of contract code and given by reference only to a callback that
581        // is generic over the lifetime of `Env`, so it can only borrow `'static` memory for that
582        // lifetime, and only dropped afterward
583        let env = unsafe { Env::with_executor_context(env_state, &executor_context) };
584        callback(&env)
585    }
586}