From 9b1274a4db499d3762256b18601762961d21d7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Holm=20Gj=C3=B8rup?= Date: Wed, 17 Dec 2025 13:28:41 +0100 Subject: [PATCH 1/3] Pass block state pointer in FFI --- .../src/Concordium/PLTScheduler.hs | 35 +++++++++++++++++-- plt-scheduler/src/block_state.rs | 7 ++++ plt-scheduler/src/ffi.rs | 30 +++++++++++++--- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/concordium-consensus/src/Concordium/PLTScheduler.hs b/concordium-consensus/src/Concordium/PLTScheduler.hs index d94113711..238cc82a1 100644 --- a/concordium-consensus/src/Concordium/PLTScheduler.hs +++ b/concordium-consensus/src/Concordium/PLTScheduler.hs @@ -5,6 +5,8 @@ -- -- Each foreign imported function must match the signature of functions found in @plt-scheduler/src/ffi.rs@. module Concordium.PLTScheduler ( + PLTBlockState, + initialPLTBlockState, executeTransaction, ) where @@ -19,13 +21,16 @@ import qualified Foreign.C.Types as FFI -- -- See @execute_transaction@ in @plt-scheduler@ rust crate for details. executeTransaction :: + -- | Block state to mutate. + PLTBlockState -> -- | Transaction payload byte string. BS.ByteString -> -- | The events produced or the reject reason. IO (Either () ()) -executeTransaction transactionPayload = do - statusCode <- BS.unsafeUseAsCStringLen transactionPayload $ \(transactionPayloadPtr, transactionPayloadLen) -> do - ffiExecuteTransaction (FFI.castPtr transactionPayloadPtr) (fromIntegral transactionPayloadLen) +executeTransaction blockState transactionPayload = do + statusCode <- withPLTBlockState blockState $ \blockStatePtr -> + BS.unsafeUseAsCStringLen transactionPayload $ \(transactionPayloadPtr, transactionPayloadLen) -> do + ffiExecuteTransaction blockStatePtr (FFI.castPtr transactionPayloadPtr) (fromIntegral transactionPayloadLen) case statusCode of 0 -> return $ Right () 1 -> return $ Left () @@ -33,9 +38,33 @@ executeTransaction transactionPayload = do foreign import ccall "ffi_execute_transaction" ffiExecuteTransaction :: + FFI.Ptr PLTBlockState -> -- | Pointer to transaction payload bytes. FFI.Ptr Word.Word8 -> -- | Byte length of transaction payload. FFI.CSize -> -- | Status code IO Word.Word8 + +-- Block state FFI + +-- | Opaque pointer to the PLT block state managed by the rust library. +-- +-- Memory is deallocated using a finalizer. +newtype PLTBlockState = PLTBlockState (FFI.ForeignPtr PLTBlockState) + +-- | Allocate new initial block state +initialPLTBlockState :: IO PLTBlockState +initialPLTBlockState = do + state <- ffiInitialPLTBlockState + PLTBlockState <$> FFI.newForeignPtr ffiFreePLTBlockState state + +foreign import ccall "ffi_initial_plt_block_state" ffiInitialPLTBlockState :: IO (FFI.Ptr PLTBlockState) +foreign import ccall unsafe "&ffi_free_plt_block_state" ffiFreePLTBlockState :: FFI.FinalizerPtr PLTBlockState + +-- | Get temporary access to the block state pointer. The pointer should not be +-- leaked from the computation. +-- +-- This ensures the finalizer is not called until the computation is over. +withPLTBlockState :: PLTBlockState -> (FFI.Ptr PLTBlockState -> IO a) -> IO a +withPLTBlockState (PLTBlockState foreignPtr) = FFI.withForeignPtr foreignPtr diff --git a/plt-scheduler/src/block_state.rs b/plt-scheduler/src/block_state.rs index 2e8dc1c1b..d82df41e8 100644 --- a/plt-scheduler/src/block_state.rs +++ b/plt-scheduler/src/block_state.rs @@ -2,6 +2,13 @@ use crate::BlockStateOperations; pub struct BlockState {} +impl BlockState { + /// Initialize a new block state. + pub fn new() -> Self { + Self {} + } +} + impl BlockStateOperations for BlockState { fn get_plt_list( &self, diff --git a/plt-scheduler/src/ffi.rs b/plt-scheduler/src/ffi.rs index 037455caf..eada70958 100644 --- a/plt-scheduler/src/ffi.rs +++ b/plt-scheduler/src/ffi.rs @@ -12,25 +12,31 @@ use libc::size_t; /// /// # Arguments /// +/// - `block_state` Unique pointer to a block state to mutate during execution. /// - `payload` Pointer to transaction payload bytes. /// - `payload_len` Byte length of transaction payload. /// /// # Safety /// +/// - Argument `block_state` must be non-null point to well-formed [`crate::block_state::BlockState`]. /// - Argument `payload` must be non-null and valid for reads for `payload_len` many bytes. #[no_mangle] -unsafe extern "C" fn ffi_execute_transaction(payload: *const u8, payload_len: size_t) -> u8 { +unsafe extern "C" fn ffi_execute_transaction( + block_state: *mut crate::block_state::BlockState, + payload: *const u8, + payload_len: size_t, +) -> u8 { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); debug_assert!(!payload.is_null(), "Payload is a null pointer."); let payload = std::slice::from_raw_parts(payload, payload_len); let mut scheduler_state = SchedulerState {}; - let mut block_state = crate::block_state::BlockState {}; - match crate::execute_transaction(&mut scheduler_state, &mut block_state, payload) { + match crate::execute_transaction(&mut scheduler_state, &mut *block_state, payload) { Ok(()) => 0, Err(crate::TransactionRejectReason) => 1, } } -/// Trackes the energy remaining and some context during the execution. +/// Tracks the energy remaining and some context during the execution. struct SchedulerState {} impl crate::SchedulerOperations for SchedulerState { fn sender_account(&self) -> concordium_base::base::AccountIndex { @@ -52,3 +58,19 @@ impl crate::SchedulerOperations for SchedulerState { todo!() } } + +// Block state FFI + +/// Allocate a new initial PLT block state. +#[no_mangle] +extern "C" fn ffi_initial_plt_block_state() -> *mut crate::block_state::BlockState { + let block_state = crate::block_state::BlockState::new(); + Box::into_raw(Box::new(block_state)) +} + +#[no_mangle] +/// Deallocate the PLT block state. +extern "C" fn ffi_free_plt_block_state(block_state: *mut crate::block_state::BlockState) { + let state = unsafe { Box::from_raw(block_state) }; + drop(state); +} From ab431d968d08b209f17a672ef8c255219aa7bea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Holm=20Gj=C3=B8rup?= Date: Fri, 19 Dec 2025 10:48:43 +0100 Subject: [PATCH 2/3] Add FFI for block state managed by rust --- .../src/Concordium/PLTScheduler.hs | 128 ++++++--- .../Concordium/PLTScheduler/PLTBlockState.hs | 149 ++++++++++ plt-scheduler/Cargo.lock | 1 + plt-scheduler/Cargo.toml | 1 + plt-scheduler/src/block_state.rs | 118 ++++---- plt-scheduler/src/block_state/blob_store.rs | 92 +++++++ plt-scheduler/src/block_state/ffi.rs | 256 ++++++++++++++++++ plt-scheduler/src/ffi.rs | 98 +++++-- plt-scheduler/src/lib.rs | 3 +- 9 files changed, 722 insertions(+), 124 deletions(-) create mode 100644 concordium-consensus/src/Concordium/PLTScheduler/PLTBlockState.hs create mode 100644 plt-scheduler/src/block_state/blob_store.rs create mode 100644 plt-scheduler/src/block_state/ffi.rs diff --git a/concordium-consensus/src/Concordium/PLTScheduler.hs b/concordium-consensus/src/Concordium/PLTScheduler.hs index 238cc82a1..b897b8c2b 100644 --- a/concordium-consensus/src/Concordium/PLTScheduler.hs +++ b/concordium-consensus/src/Concordium/PLTScheduler.hs @@ -1,13 +1,11 @@ -{-# LANGUAGE OverloadedStrings #-} - -- | -- Bindings into the @plt-scheduler@ Rust library exposing safe wrappers. -- -- Each foreign imported function must match the signature of functions found in @plt-scheduler/src/ffi.rs@. module Concordium.PLTScheduler ( - PLTBlockState, - initialPLTBlockState, executeTransaction, + ExecutionOutcome (..), + ExecutionAccepts (..), ) where import qualified Data.ByteString as BS @@ -15,56 +13,108 @@ import qualified Data.ByteString.Unsafe as BS import qualified Data.Word as Word import qualified Foreign as FFI import qualified Foreign.C.Types as FFI +import Control.Monad.IO.Class (liftIO) + +import qualified Concordium.PLTScheduler.PLTBlockState as PLTBlockState +import qualified Concordium.Types as Types +import qualified Data.FixedByteString as FixedByteString +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI -- | Execute a transaction payload modifying the `block_state` accordingly. --- The caller must ensure to rollback state changes in case of the transaction being rejected. -- -- See @execute_transaction@ in @plt-scheduler@ rust crate for details. -executeTransaction :: +executeTransaction :: (BlobStore.MonadBlobStore m) => -- | Block state to mutate. - PLTBlockState -> + PLTBlockState.PLTBlockState -> -- | Transaction payload byte string. BS.ByteString -> - -- | The events produced or the reject reason. - IO (Either () ()) -executeTransaction blockState transactionPayload = do - statusCode <- withPLTBlockState blockState $ \blockStatePtr -> - BS.unsafeUseAsCStringLen transactionPayload $ \(transactionPayloadPtr, transactionPayloadLen) -> do - ffiExecuteTransaction blockStatePtr (FFI.castPtr transactionPayloadPtr) (fromIntegral transactionPayloadLen) - case statusCode of - 0 -> return $ Right () - 1 -> return $ Left () - _ -> error "Unexpected status code from calling 'ffiExecuteTransaction'" + -- | The account index of the account which signed as the sender of the transaction. + Types.AccountIndex -> + -- | The account address of the account which signed as the sender of the transaction. + Types.AccountAddress -> + -- | Remaining energy. + Types.Energy -> + -- | Outcome of the execution + m ExecutionOutcome +executeTransaction + blockState + transactionPayload + senderAccountIndex + (Types.AccountAddress senderAccountAddress) + remainingEnergy = do + loadCallback <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \remainingEnergyOut -> FFI.alloca $ \updatedBlockStatePtrOut -> do + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState blockState $ \blockStatePtr -> + FixedByteString.withPtrReadOnly senderAccountAddress $ \senderAccountAddressPtr -> + BS.unsafeUseAsCStringLen transactionPayload $ + \(transactionPayloadPtr, transactionPayloadLen) -> + ffiExecuteTransaction + loadCallback + blockStatePtr + (FFI.castPtr transactionPayloadPtr) + (fromIntegral transactionPayloadLen) + (fromIntegral senderAccountIndex) + senderAccountAddressPtr + (fromIntegral remainingEnergy) + updatedBlockStatePtrOut + remainingEnergyOut + -- Process the and construct the outcome + newRemainingEnergy <- fromIntegral <$> FFI.peek remainingEnergyOut + status <- case statusCode of + 0 -> do + updatedBlockState <- FFI.peek updatedBlockStatePtrOut >>= PLTBlockState.wrapFFIPtr + return $ + Right + ExecutionAccepts + { eaUpdatedPLTBlockState = updatedBlockState, + eaEvents = () + } + 1 -> return $ Left () + _ -> error "Unexpected status code from calling 'ffiExecuteTransaction'" + return + ExecutionOutcome + { erRemainingEnergy = newRemainingEnergy, + erStatus = status + } foreign import ccall "ffi_execute_transaction" ffiExecuteTransaction :: - FFI.Ptr PLTBlockState -> + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Pointer to the starting block state. + FFI.Ptr PLTBlockState.PLTBlockState -> -- | Pointer to transaction payload bytes. FFI.Ptr Word.Word8 -> -- | Byte length of transaction payload. FFI.CSize -> + -- | The account index of the account which signed as the sender of the transaction. + Word.Word64 -> + -- | Pointer to 32 bytes representing the account address of the account which signed as the + -- sender of the transaction. + FFI.Ptr Word.Word8 -> + -- | Remaining energy + Word.Word64 -> + -- | Output location for the updated block state. + FFI.Ptr (FFI.Ptr PLTBlockState.PLTBlockState) -> + -- | Output location for the remaining energy after execution. + FFI.Ptr Word.Word64 -> -- | Status code IO Word.Word8 --- Block state FFI - --- | Opaque pointer to the PLT block state managed by the rust library. --- --- Memory is deallocated using a finalizer. -newtype PLTBlockState = PLTBlockState (FFI.ForeignPtr PLTBlockState) - --- | Allocate new initial block state -initialPLTBlockState :: IO PLTBlockState -initialPLTBlockState = do - state <- ffiInitialPLTBlockState - PLTBlockState <$> FFI.newForeignPtr ffiFreePLTBlockState state +-- | The outcome of executing a transaction using the PLT scheduler. +data ExecutionOutcome = ExecutionOutcome + { -- | The amount of energy remaining after the execution. + erRemainingEnergy :: Types.Energy, + -- | The resulting execution status. + erStatus :: Either () ExecutionAccepts + } -foreign import ccall "ffi_initial_plt_block_state" ffiInitialPLTBlockState :: IO (FFI.Ptr PLTBlockState) -foreign import ccall unsafe "&ffi_free_plt_block_state" ffiFreePLTBlockState :: FFI.FinalizerPtr PLTBlockState - --- | Get temporary access to the block state pointer. The pointer should not be --- leaked from the computation. --- --- This ensures the finalizer is not called until the computation is over. -withPLTBlockState :: PLTBlockState -> (FFI.Ptr PLTBlockState -> IO a) -> IO a -withPLTBlockState (PLTBlockState foreignPtr) = FFI.withForeignPtr foreignPtr +-- | Additional execution outcome when the transaction gets accepted by the PLT scheduler. +data ExecutionAccepts = ExecutionAccepts + { -- | The updated PLT block state after the execution + eaUpdatedPLTBlockState :: PLTBlockState.PLTBlockState, + -- | Events produced during the execution + eaEvents :: () + } diff --git a/concordium-consensus/src/Concordium/PLTScheduler/PLTBlockState.hs b/concordium-consensus/src/Concordium/PLTScheduler/PLTBlockState.hs new file mode 100644 index 000000000..494e11b55 --- /dev/null +++ b/concordium-consensus/src/Concordium/PLTScheduler/PLTBlockState.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE DerivingVia #-} + +-- | Bindings to the PLT block state implementation found in @plt-scheduler/block_state.rs@. +module Concordium.PLTScheduler.PLTBlockState ( + PLTBlockState, + empty, + wrapFFIPtr, + withPLTBlockState, + migrate, + Hash, + -- | Get the inner @SHA256.Hash@. + innerSha256Hash, +) where + +import qualified Data.Serialize as Serialize +import qualified Foreign as FFI + +import qualified Concordium.Crypto.SHA256 as SHA256 +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore +import qualified Concordium.Types.HashableTo as Hashable +import Control.Monad.Trans (lift, liftIO) +import qualified Data.FixedByteString as FixedByteString + +-- | Opaque pointer to a immutable PLT block state save-point managed by the rust library. +-- +-- Memory is deallocated using a finalizer. +newtype PLTBlockState = PLTBlockState (FFI.ForeignPtr PLTBlockState) + +-- | Helper function to convert a raw pointer passed by the Rust library into a `PLTBlockState` object. +wrapFFIPtr :: FFI.Ptr PLTBlockState -> IO PLTBlockState +wrapFFIPtr blockStatePtr = PLTBlockState <$> FFI.newForeignPtr ffiFreePLTBlockState blockStatePtr + +-- | Deallocate a pointer to `PLTBlockState`. +foreign import ccall unsafe "&ffi_free_plt_block_state" + ffiFreePLTBlockState :: FFI.FinalizerPtr PLTBlockState + +-- | Get temporary access to the block state pointer. The pointer should not be +-- leaked from the computation. +-- +-- This ensures the finalizer is not called until the computation is over. +withPLTBlockState :: PLTBlockState -> (FFI.Ptr PLTBlockState -> IO a) -> IO a +withPLTBlockState (PLTBlockState foreignPtr) = FFI.withForeignPtr foreignPtr + +-- | Allocate new empty block state +empty :: (BlobStore.MonadBlobStore m) => m PLTBlockState +empty = liftIO $ do + state <- ffiEmptyPLTBlockState + wrapFFIPtr state + +foreign import ccall "ffi_empty_plt_block_state" + ffiEmptyPLTBlockState :: IO (FFI.Ptr PLTBlockState) + +instance (BlobStore.MonadBlobStore m) => BlobStore.BlobStorable m PLTBlockState where + load = do + blobRef <- Serialize.get + pure $! do + loadCallback <- fst <$> BlobStore.getCallbacks + liftIO $! do + blockState <- ffiLoadPLTBlockState loadCallback blobRef + wrapFFIPtr blockState + storeUpdate pltBlockState = do + storeCallback <- snd <$> BlobStore.getCallbacks + blobRef <- liftIO $ withPLTBlockState pltBlockState $ ffiStorePLTBlockState storeCallback + return (Serialize.put blobRef, pltBlockState) + +-- | Load PLT block state from the given disk reference. +foreign import ccall "ffi_load_plt_block_state" + ffiLoadPLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Reference in the blob store. + BlobStore.BlobRef PLTBlockState -> + -- | Pointer to the loaded block state. + IO (FFI.Ptr PLTBlockState) + +-- | Write out the block state using the provided callback, and return a `BlobRef`. +foreign import ccall "ffi_store_plt_block_state" + ffiStorePLTBlockState :: + -- | The provided closure is called to write data to blob store. + FFI.StoreCallback -> + -- | Pointer to the block state to write. + FFI.Ptr PLTBlockState -> + -- | New reference in the blob store. + IO (BlobStore.BlobRef PLTBlockState) + +instance (BlobStore.MonadBlobStore m) => BlobStore.Cacheable m PLTBlockState where + cache blockState = do + loadCallback <- fst <$> BlobStore.getCallbacks + liftIO $! withPLTBlockState blockState (ffiCachePLTBlockState loadCallback) + return blockState + +-- | Cache block state into memory. +foreign import ccall "ffi_cache_plt_block_state" + ffiCachePLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Pointer to the block state to cache into memory. + FFI.Ptr PLTBlockState -> + IO () + +-- | The hash of some `PLTBlockState`. +newtype Hash = Hash {innerSha256Hash :: SHA256.Hash} + deriving newtype (Eq, Ord, Show, Serialize.Serialize) + +instance (BlobStore.MonadBlobStore m) => Hashable.MHashableTo m Hash PLTBlockState where + getHashM blockState = do + loadCallback <- fst <$> BlobStore.getCallbacks + ((), hash) <- + liftIO $ + withPLTBlockState blockState $ + FixedByteString.createWith . ffiHashPLTBlockState loadCallback + return $ Hash (SHA256.Hash hash) + +-- | Compute the hash of the block state. +foreign import ccall "ffi_hash_plt_block_state" + ffiHashPLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Pointer to the block state to write. + FFI.Ptr PLTBlockState -> + -- | Pointer to write destination of the hash + FFI.Ptr FFI.Word8 -> + IO () + +-- | Run migration during a protocol update. +migrate :: + (BlobStore.SupportMigration m t) => + -- | Current block state + PLTBlockState -> + -- | New migrated block state + t m PLTBlockState +migrate currentState = do + loadCallback <- fst <$> lift BlobStore.getCallbacks + storeCallback <- snd <$> BlobStore.getCallbacks + newState <- liftIO $ withPLTBlockState currentState $ ffiMigratePLTBlockState loadCallback storeCallback + liftIO $ PLTBlockState <$> FFI.newForeignPtr ffiFreePLTBlockState newState + +-- | Migrate PLT block state from one blob store to another. +foreign import ccall "ffi_migrate_plt_block_state" + ffiMigratePLTBlockState :: + -- | Called to read data from the old blob store. + FFI.LoadCallback -> + -- | Called to write data to the new blob store. + FFI.StoreCallback -> + -- | Pointer to the old block state. + FFI.Ptr PLTBlockState -> + -- | Pointer to the new block state. + IO (FFI.Ptr PLTBlockState) diff --git a/plt-scheduler/Cargo.lock b/plt-scheduler/Cargo.lock index d56f3bca3..5fb2860c0 100644 --- a/plt-scheduler/Cargo.lock +++ b/plt-scheduler/Cargo.lock @@ -1080,6 +1080,7 @@ dependencies = [ "derive_more 2.1.0", "libc", "plt-deployment-unit", + "thiserror 2.0.17", ] [[package]] diff --git a/plt-scheduler/Cargo.toml b/plt-scheduler/Cargo.toml index de8cca463..567464081 100644 --- a/plt-scheduler/Cargo.toml +++ b/plt-scheduler/Cargo.toml @@ -14,3 +14,4 @@ concordium_base = {path = "../concordium-base/rust-src/concordium_base"} plt-deployment-unit = {path = "../plt-deployment-unit"} derive_more = { version = "2.1.0", features = ["into", "from"] } libc = { version = "0.2", optional = true } +thiserror = "2.0.17" diff --git a/plt-scheduler/src/block_state.rs b/plt-scheduler/src/block_state.rs index d82df41e8..62887590e 100644 --- a/plt-scheduler/src/block_state.rs +++ b/plt-scheduler/src/block_state.rs @@ -1,83 +1,91 @@ -use crate::BlockStateOperations; +//! This module contains the [`BlockState`] which provides an implementation of [`BlockStateOperations`]. +//! -pub struct BlockState {} +pub mod blob_store; +#[cfg(feature = "ffi")] +pub mod ffi; -impl BlockState { - /// Initialize a new block state. - pub fn new() -> Self { - Self {} - } -} - -impl BlockStateOperations for BlockState { - fn get_plt_list( - &self, - ) -> impl std::iter::Iterator { - // TODO implement this. The implementation below is just to help the type checker infer - // enough for this to compile - Vec::new().into_iter() - } +pub enum PltBlockStateHashMarker {} +pub type PltBlockStateHash = concordium_base::hashes::HashBytes; - fn get_token_index( - &self, - _token_id: concordium_base::protocol_level_tokens::TokenId, - ) -> Option { - todo!() - } +/// Immutable block state save-point. +/// +/// This is a safe wrapper around a [`BlockState`] ensuring further mutations can only be done by +/// unwrapping using [`BlockStateSavepoint::new_generation`] which creates a new generation. +#[derive(Debug)] +pub struct BlockStateSavepoint { + /// The inner block state, which will not be mutated further for this generation. + block_state: BlockState, +} - fn get_mutable_token_state(&self, _token_index: crate::TokenIndex) -> crate::MutableTokenState { - todo!() +impl BlockStateSavepoint { + /// Initialize a new block state. + pub fn empty() -> Self { + Self { + block_state: BlockState::empty(), + } } - fn get_token_configuration(&self, _token_index: crate::TokenIndex) -> crate::PLTConfiguration { + /// Compute the hash. + pub fn hash(&self, _loader: impl blob_store::BackingStoreLoad) -> PltBlockStateHash { todo!() } - fn get_token_circulating_supply( - &self, - _token_index: crate::TokenIndex, - ) -> plt_deployment_unit::host_interface::TokenRawAmount { + /// Store a PLT block state in a blob store. + pub fn store_update( + &mut self, + _storer: &mut impl blob_store::BackingStoreStore, + ) -> blob_store::StoreResult { todo!() } - fn set_token_circulating_supply( + /// Migrate the PLT block state from one blob store to another. + pub fn migrate( &mut self, - _token_index: crate::TokenIndex, - _circulating_supply: plt_deployment_unit::host_interface::TokenRawAmount, - ) { + _loader: &impl blob_store::BackingStoreLoad, + _storer: &mut impl blob_store::BackingStoreStore, + ) -> blob_store::LoadStoreResult { todo!() } - fn create_token(&mut self, _configuration: crate::PLTConfiguration) -> crate::TokenIndex { + /// Cache the block state in memory. + pub fn cache(&mut self, _loader: &impl blob_store::BackingStoreLoad) { todo!() } - fn update_token_account_balance( - &mut self, - _token_index: crate::TokenIndex, - _account_index: concordium_base::base::AccountIndex, - _amount_delta: crate::TokenAmountDelta, - ) -> Result<(), crate::OverflowError> { - todo!() + /// Construct a new generation block state which can be mutated without affecting this + /// save-point. + pub fn new_generation(&self) -> BlockState { + let mut block_state = self.block_state.clone(); + block_state.generation += 1; + block_state } +} - fn touch_token_account( - &mut self, - _token_index: crate::TokenIndex, - _account_index: concordium_base::base::AccountIndex, - ) -> bool { +impl blob_store::Loadable for BlockStateSavepoint { + fn load( + _loader: &mut F, + _source: &mut S, + ) -> blob_store::LoadResult { todo!() } +} - fn increment_plt_update_sequence_number(&mut self) { - todo!() +/// Block state providing the various block state operations. +#[derive(Debug, Clone)] +pub struct BlockState { + /// The generation counter for the block state. + generation: u64, +} + +impl BlockState { + /// Construct an empty block state. + fn empty() -> Self { + BlockState { generation: 0 } } - fn set_token_state( - &mut self, - _token_index: crate::TokenIndex, - _mutable_token_state: crate::MutableTokenState, - ) { - todo!() + /// Consume the mutable block state and create a immutable save-point. + pub fn savepoint(self) -> BlockStateSavepoint { + BlockStateSavepoint { block_state: self } } } diff --git a/plt-scheduler/src/block_state/blob_store.rs b/plt-scheduler/src/block_state/blob_store.rs new file mode 100644 index 000000000..2b66c4af2 --- /dev/null +++ b/plt-scheduler/src/block_state/blob_store.rs @@ -0,0 +1,92 @@ +// Temporarily allow dead_code until we have block state implemented. +#![allow(dead_code)] + +/// Reference to a storage location where an item may be retrieved. +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, derive_more::From, derive_more::Into)] +#[repr(transparent)] +pub struct Reference { + pub(crate) reference: u64, +} + +/// Trait implemented by types that can be used to store binary data, and return +/// a handle for loading data. Dual to [`BackingStoreLoad`]. +pub trait BackingStoreStore { + /// Store the provided value and return a reference that can be used + /// to load it. + fn store_raw(&mut self, data: &[u8]) -> Result; +} + +/// Trait implemented by types that can load data from given locations. +/// Dual to [`BackingStoreStore`]. +pub trait BackingStoreLoad { + type R: AsRef<[u8]>; + /// Load the provided value from the given location. The implementation of + /// this should match [BackingStoreStore::store_raw]. + fn load_raw(&mut self, location: Reference) -> LoadResult; +} + +/// A trait implemented by types that can be loaded from a [BackingStoreLoad] +/// storage. +pub trait Loadable: Sized { + fn load( + loader: &mut F, + source: &mut S, + ) -> LoadResult; + + fn load_from_location( + loader: &mut F, + location: Reference, + ) -> LoadResult { + let mut source = std::io::Cursor::new(loader.load_raw(location)?); + Self::load(loader, &mut source) + } +} + +/// An error that may occur when writing data to persistent storage. +#[derive(Debug, thiserror::Error)] +pub enum WriteError { + #[error("{0}")] + IOError(#[from] std::io::Error), +} + +/// Result of storing data in persistent storage. +pub type StoreResult = Result; + +/// An error that may occur when loading data from persistent storage. +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error("{0}")] + IOError(#[from] std::io::Error), + // #[error("Incorrect tag")] + // IncorrectTag { + // // The tag that was provided. + // tag: u8, + // }, + // #[error("Out of bounds read.")] + // OutOfBoundsRead, +} + +/// Result of loading data from persistent storage. +pub type LoadResult = Result; + +#[derive(Debug, thiserror::Error)] +/// An error that may occur when storing or loading data from persistent +/// storage. +pub enum LoadWriteError { + #[error("Write error: {0}")] + Write(#[from] WriteError), + #[error("Load error: {0}")] + Load(#[from] LoadError), +} + +/// In the context where the LoadWriteError is used we want to propagate +/// write errors out. So this implementation is the more useful one as opposed +/// to the implementation which maps io errors through the [`LoadError`]. +impl From for LoadWriteError { + fn from(err: std::io::Error) -> Self { + Self::Write(err.into()) + } +} + +/// Result of loading or storing data from or to persistent storage. +pub type LoadStoreResult = Result; diff --git a/plt-scheduler/src/block_state/ffi.rs b/plt-scheduler/src/block_state/ffi.rs new file mode 100644 index 000000000..c3e12273a --- /dev/null +++ b/plt-scheduler/src/block_state/ffi.rs @@ -0,0 +1,256 @@ +//! This module provides a C ABI for the block state implementation of this library. +//! +//! It is only available if the `ffi` feature is enabled. + +use crate::block_state::{self, blob_store}; +use libc::size_t; + +/// A [loader](BackingStoreLoad) implemented by an external function. +/// This is the dual to [`StoreCallback`] +pub type LoadCallback = extern "C" fn(blob_store::Reference) -> *mut Vec; +/// A [storer](BackingStoreStore) implemented by an external function. +/// The function is passed a pointer to data to store, and the size of data. It +/// should return the location where the data can be loaded via a +/// [`LoadCallback`]. +pub type StoreCallback = extern "C" fn(data: *const u8, len: size_t) -> blob_store::Reference; + +impl blob_store::BackingStoreStore for StoreCallback { + #[inline] + fn store_raw(&mut self, data: &[u8]) -> blob_store::StoreResult { + Ok(self(data.as_ptr(), data.len())) + } +} + +impl blob_store::BackingStoreLoad for LoadCallback { + type R = Vec; + + #[inline] + fn load_raw(&mut self, location: blob_store::Reference) -> blob_store::LoadResult { + Ok(*unsafe { Box::from_raw(self(location)) }) + } +} + +/// Allocate a new empty PLT block state. +/// +/// It is up to the caller to free this memory using [`ffi_free_plt_block_state`]. +#[no_mangle] +extern "C" fn ffi_empty_plt_block_state() -> *mut block_state::BlockStateSavepoint { + let block_state = block_state::BlockStateSavepoint::empty(); + Box::into_raw(Box::new(block_state)) +} + +/// Deallocate the PLT block state. +/// +/// # Arguments +/// +/// - `block_state` Unique pointer to the PLT block state. +/// +/// # Safety +/// +/// Caller must ensure: +/// +/// - Argument `block_state` cannot be referenced by anyone else. +/// - Freeing is only ever done once. +#[no_mangle] +unsafe extern "C" fn ffi_free_plt_block_state(block_state: *mut block_state::BlockStateSavepoint) { + let state = Box::from_raw(block_state); + drop(state); +} + +/// Compute the hash of the PLT block state. +/// +/// # Arguments +/// +/// - `block_state` Pointer to the PLT block state to compute a hash for. +/// - `destination` Unique pointer with location to write the 32 bytes for the hash. +/// +/// # Safety +/// +/// Caller must ensure `destination` can hold 32 bytes for the hash. +#[no_mangle] +unsafe extern "C" fn ffi_hash_plt_block_state( + load_callback: LoadCallback, + destination: *mut u8, + block_state: *const block_state::BlockStateSavepoint, +) { + let block_state = &*block_state; + let hash = block_state.hash(load_callback); + std::ptr::copy_nonoverlapping(hash.as_ptr(), destination, hash.len()); +} + +/// Load a PLT block state from the blob store. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `blob_ref` Blob store reference to load the block state from. +/// +/// # Safety +/// +/// Caller must ensure `blob_ref` is a valid reference in the blob store. +#[no_mangle] +extern "C" fn ffi_load_plt_block_state( + mut load_callback: LoadCallback, + blob_ref: blob_store::Reference, +) -> *mut block_state::BlockStateSavepoint { + match blob_store::Loadable::load_from_location(&mut load_callback, blob_ref) { + Ok(block_state) => Box::into_raw(Box::new(block_state)), + Err(_) => std::ptr::null_mut(), + } +} + +/// Store a PLT block state in the blob store. +/// +/// # Arguments +/// +/// - `store_callback` External function to call for storing bytes in the blob store returning a +/// reference. +/// - `block_state` The block state to store in the blob store. +/// +/// # Safety +/// +/// Caller must ensure `block_state` is non-null and points to a valid block state. +#[no_mangle] +unsafe extern "C" fn ffi_store_plt_block_state( + mut store_callback: StoreCallback, + block_state: *mut block_state::BlockStateSavepoint, +) -> blob_store::Reference { + let block_state = &mut *block_state; + match block_state.store_update(&mut store_callback) { + Ok(r) => r, + Err(_) => unreachable!( + "Storing the block state can only fail if the writer fails. This is assumed not to happen." + ), + } +} + +/// Migrate the PLT block state from one blob store to another. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `store_callback` External function to call for storing bytes in the blob store returning a +/// reference. +/// - `block_state` The block state to store in the blob store. +/// +/// # Safety +/// +/// Caller must ensure `block_state` is non-null and points to a valid block state. +#[no_mangle] +unsafe extern "C" fn ffi_migrate_plt_block_state( + load_callback: LoadCallback, + mut store_callback: StoreCallback, + block_state: *mut block_state::BlockStateSavepoint, +) -> *mut block_state::BlockStateSavepoint { + let block_state = &mut *block_state; + match block_state.migrate(&load_callback, &mut store_callback) { + Ok(new_block_state) => Box::into_raw(Box::new(new_block_state)), + Err(_) => std::ptr::null_mut(), + } +} + +/// Cache the PLT block state into memory. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `block_state` The block state to store in the blob store. +/// +/// # Safety +/// +/// Caller must ensure `block_state` is non-null and points to a valid block state. +#[no_mangle] +unsafe extern "C" fn ffi_cache_plt_block_state( + load_callback: LoadCallback, + block_state: *mut block_state::BlockStateSavepoint, +) { + let block_state = &mut *block_state; + block_state.cache(&load_callback) +} + +/// Runtime/execution state relevant for providing an implementation of +/// [`crate::BlockStateOperations`]. +/// +/// This is needed since callbacks are only available during the execution time. +#[derive(Debug)] +pub(crate) struct ExecutionTimeBlockState { + /// The library block state implementation. + pub(crate) inner_block_state: block_state::BlockState, + // Temporary disable warning until we have the implementation below started. + #[expect(dead_code)] + /// External function for reading from the blob store. + pub(crate) load_callback: LoadCallback, +} + +impl crate::BlockStateOperations for ExecutionTimeBlockState { + fn get_plt_list( + &self, + ) -> impl std::iter::Iterator { + // TODO implement this. The implementation below is just to help the type checker infer + // enough for this to compile. + Vec::new().into_iter() + } + + fn get_token_index( + &self, + _token_id: concordium_base::protocol_level_tokens::TokenId, + ) -> Option { + todo!() + } + + fn get_mutable_token_state(&self, _token_index: crate::TokenIndex) -> crate::MutableTokenState { + todo!() + } + + fn get_token_configuration(&self, _token_index: crate::TokenIndex) -> crate::PLTConfiguration { + todo!() + } + + fn get_token_circulating_supply( + &self, + _token_index: crate::TokenIndex, + ) -> plt_deployment_unit::host_interface::TokenRawAmount { + todo!() + } + + fn set_token_circulating_supply( + &mut self, + _token_index: crate::TokenIndex, + _circulating_supply: plt_deployment_unit::host_interface::TokenRawAmount, + ) { + todo!() + } + + fn create_token(&mut self, _configuration: crate::PLTConfiguration) -> crate::TokenIndex { + todo!() + } + + fn update_token_account_balance( + &mut self, + _token_index: crate::TokenIndex, + _account_index: concordium_base::base::AccountIndex, + _amount_delta: crate::TokenAmountDelta, + ) -> Result<(), crate::OverflowError> { + todo!() + } + + fn touch_token_account( + &mut self, + _token_index: crate::TokenIndex, + _account_index: concordium_base::base::AccountIndex, + ) -> bool { + todo!() + } + + fn increment_plt_update_sequence_number(&mut self) { + todo!() + } + + fn set_token_state( + &mut self, + _token_index: crate::TokenIndex, + _mutable_token_state: crate::MutableTokenState, + ) { + todo!() + } +} diff --git a/plt-scheduler/src/ffi.rs b/plt-scheduler/src/ffi.rs index eada70958..5d928ba5c 100644 --- a/plt-scheduler/src/ffi.rs +++ b/plt-scheduler/src/ffi.rs @@ -1,6 +1,8 @@ -//! This module provides a C ABI for this library. +//! This module provides a C ABI for the top-level functions of this library. +//! //! It is only available if the `ffi` feature is enabled. +use crate::block_state; use libc::size_t; /// C-binding for calling [`crate::execute_transaction`]. @@ -12,65 +14,105 @@ use libc::size_t; /// /// # Arguments /// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. /// - `block_state` Unique pointer to a block state to mutate during execution. /// - `payload` Pointer to transaction payload bytes. /// - `payload_len` Byte length of transaction payload. +/// - `sender_account_index` The account index of the account which signed as the sender of the transaction. +/// - `sender_account_address` The account address of the account which signed as the sender of the transaction. +/// - `remaining_energy` The remaining energy at the start of the execution. +/// - `block_state_out` Location for writing the pointer of the updated block state. +/// - `remaining_energy_out` Location for writing the remaining energy after the execution. /// /// # Safety /// /// - Argument `block_state` must be non-null point to well-formed [`crate::block_state::BlockState`]. /// - Argument `payload` must be non-null and valid for reads for `payload_len` many bytes. +/// - Argument `sender_account_address` must be non-null and valid for reads for 32 bytes. #[no_mangle] unsafe extern "C" fn ffi_execute_transaction( - block_state: *mut crate::block_state::BlockState, + load_callback: block_state::ffi::LoadCallback, + block_state: *const block_state::BlockStateSavepoint, payload: *const u8, payload_len: size_t, + sender_account_index: u64, + sender_account_address: *const u8, + remaining_energy: u64, + block_state_out: *mut *const block_state::BlockStateSavepoint, + remaining_energy_out: *mut u64, ) -> u8 { debug_assert!(!block_state.is_null(), "Block state is a null pointer."); debug_assert!(!payload.is_null(), "Payload is a null pointer."); + debug_assert!( + !sender_account_address.is_null(), + "Sender account address is a null pointer." + ); let payload = std::slice::from_raw_parts(payload, payload_len); - let mut scheduler_state = SchedulerState {}; - match crate::execute_transaction(&mut scheduler_state, &mut *block_state, payload) { - Ok(()) => 0, - Err(crate::TransactionRejectReason) => 1, + let sender_account_address = { + let mut bytes = [0u8; concordium_base::contracts_common::ACCOUNT_ADDRESS_SIZE]; + std::ptr::copy_nonoverlapping( + sender_account_address, + bytes.as_mut_ptr(), + concordium_base::contracts_common::ACCOUNT_ADDRESS_SIZE, + ); + concordium_base::contracts_common::AccountAddress(bytes) + }; + let mut scheduler_state = SchedulerState { + remaining_energy: remaining_energy.into(), + sender_account_index: sender_account_index.into(), + sender_account_address, + }; + let mut block_state = block_state::ffi::ExecutionTimeBlockState { + inner_block_state: (*block_state).new_generation(), + load_callback, + }; + let result = crate::execute_transaction(&mut scheduler_state, &mut block_state, payload); + let block_state = block_state.inner_block_state; + *remaining_energy_out = scheduler_state.remaining_energy.into(); + match result { + Ok(()) => { + *block_state_out = Box::into_raw(Box::new(block_state.savepoint())); + 0 + } + Err(crate::TransactionRejectReason) => { + *block_state_out = std::ptr::null(); + 1 + } } } /// Tracks the energy remaining and some context during the execution. -struct SchedulerState {} +struct SchedulerState { + /// The remaining energy tracked spent during the execution. + remaining_energy: concordium_base::base::Energy, + /// The account index of the account which signed as the sender of the transaction. + sender_account_index: concordium_base::base::AccountIndex, + /// The account address of the account which signed as the sender of the transaction. + sender_account_address: concordium_base::contracts_common::AccountAddress, +} impl crate::SchedulerOperations for SchedulerState { fn sender_account(&self) -> concordium_base::base::AccountIndex { - todo!() + self.sender_account_index } fn sender_account_address(&self) -> concordium_base::contracts_common::AccountAddress { - todo!() + self.sender_account_address } fn get_energy(&self) -> concordium_base::base::Energy { - todo!() + self.remaining_energy } fn tick_energy( &mut self, - _energy: concordium_base::base::Energy, + energy: concordium_base::base::Energy, ) -> Result<(), crate::OutOfEnergyError> { - todo!() + if let Some(remaining_energy) = self.remaining_energy.checked_sub(energy) { + self.remaining_energy = remaining_energy; + Ok(()) + } else { + self.remaining_energy = 0.into(); + Err(crate::OutOfEnergyError) + } } } - -// Block state FFI - -/// Allocate a new initial PLT block state. -#[no_mangle] -extern "C" fn ffi_initial_plt_block_state() -> *mut crate::block_state::BlockState { - let block_state = crate::block_state::BlockState::new(); - Box::into_raw(Box::new(block_state)) -} - -#[no_mangle] -/// Deallocate the PLT block state. -extern "C" fn ffi_free_plt_block_state(block_state: *mut crate::block_state::BlockState) { - let state = unsafe { Box::from_raw(block_state) }; - drop(state); -} diff --git a/plt-scheduler/src/lib.rs b/plt-scheduler/src/lib.rs index 38b4c4123..1c7b3b896 100644 --- a/plt-scheduler/src/lib.rs +++ b/plt-scheduler/src/lib.rs @@ -3,11 +3,10 @@ use concordium_base::id::types::AccountAddress; use concordium_base::protocol_level_tokens::TokenId; use plt_deployment_unit::host_interface::TokenRawAmount; -mod block_state; +pub mod block_state; #[cfg(feature = "ffi")] mod ffi; - // Placeholder types to be defined or replaced with types from other crates. pub type MutableTokenState = (); From 47c9a8854d6562a496e073f80832148841120fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Holm=20Gj=C3=B8rup?= Date: Fri, 19 Dec 2025 15:30:19 +0100 Subject: [PATCH 3/3] Introduce callback for updating the account token balance from rust --- .../src/Concordium/PLTScheduler.hs | 134 +++++++++++++----- plt-scheduler/src/block_state/ffi.rs | 52 ++++++- plt-scheduler/src/ffi.rs | 3 + plt-scheduler/src/lib.rs | 2 +- 4 files changed, 149 insertions(+), 42 deletions(-) diff --git a/concordium-consensus/src/Concordium/PLTScheduler.hs b/concordium-consensus/src/Concordium/PLTScheduler.hs index b897b8c2b..341456a71 100644 --- a/concordium-consensus/src/Concordium/PLTScheduler.hs +++ b/concordium-consensus/src/Concordium/PLTScheduler.hs @@ -6,27 +6,33 @@ module Concordium.PLTScheduler ( executeTransaction, ExecutionOutcome (..), ExecutionAccepts (..), + UpdateTokenAccountBalanceCallback, ) where +import Control.Monad.IO.Class (liftIO) import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BS import qualified Data.Word as Word import qualified Foreign as FFI import qualified Foreign.C.Types as FFI -import Control.Monad.IO.Class (liftIO) +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens as Tokens import qualified Concordium.PLTScheduler.PLTBlockState as PLTBlockState import qualified Concordium.Types as Types +import qualified Concordium.Types.Tokens as Tokens import qualified Data.FixedByteString as FixedByteString -import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore -import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI -- | Execute a transaction payload modifying the `block_state` accordingly. -- -- See @execute_transaction@ in @plt-scheduler@ rust crate for details. -executeTransaction :: (BlobStore.MonadBlobStore m) => +executeTransaction :: + (BlobStore.MonadBlobStore m) => -- | Block state to mutate. PLTBlockState.PLTBlockState -> + -- | Callback for updating the token balance of an account. + UpdateTokenAccountBalanceCallback -> -- | Transaction payload byte string. BS.ByteString -> -- | The account index of the account which signed as the sender of the transaction. @@ -39,50 +45,55 @@ executeTransaction :: (BlobStore.MonadBlobStore m) => m ExecutionOutcome executeTransaction blockState + updateTokenAccountBalanceCallback transactionPayload senderAccountIndex (Types.AccountAddress senderAccountAddress) remainingEnergy = do loadCallback <- fst <$> BlobStore.getCallbacks liftIO $ FFI.alloca $ \remainingEnergyOut -> FFI.alloca $ \updatedBlockStatePtrOut -> do - -- Invoke the ffi call - statusCode <- PLTBlockState.withPLTBlockState blockState $ \blockStatePtr -> - FixedByteString.withPtrReadOnly senderAccountAddress $ \senderAccountAddressPtr -> - BS.unsafeUseAsCStringLen transactionPayload $ - \(transactionPayloadPtr, transactionPayloadLen) -> - ffiExecuteTransaction - loadCallback - blockStatePtr - (FFI.castPtr transactionPayloadPtr) - (fromIntegral transactionPayloadLen) - (fromIntegral senderAccountIndex) - senderAccountAddressPtr - (fromIntegral remainingEnergy) - updatedBlockStatePtrOut - remainingEnergyOut - -- Process the and construct the outcome - newRemainingEnergy <- fromIntegral <$> FFI.peek remainingEnergyOut - status <- case statusCode of - 0 -> do - updatedBlockState <- FFI.peek updatedBlockStatePtrOut >>= PLTBlockState.wrapFFIPtr - return $ - Right - ExecutionAccepts - { eaUpdatedPLTBlockState = updatedBlockState, - eaEvents = () - } - 1 -> return $ Left () - _ -> error "Unexpected status code from calling 'ffiExecuteTransaction'" - return - ExecutionOutcome - { erRemainingEnergy = newRemainingEnergy, - erStatus = status - } + updateTokenAccountBalanceCallbackPtr <- wrapUpdateTokenAccountBalanceCallback updateTokenAccountBalanceCallback + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState blockState $ \blockStatePtr -> + FixedByteString.withPtrReadOnly senderAccountAddress $ \senderAccountAddressPtr -> + BS.unsafeUseAsCStringLen transactionPayload $ + \(transactionPayloadPtr, transactionPayloadLen) -> + ffiExecuteTransaction + loadCallback + updateTokenAccountBalanceCallbackPtr + blockStatePtr + (FFI.castPtr transactionPayloadPtr) + (fromIntegral transactionPayloadLen) + (fromIntegral senderAccountIndex) + senderAccountAddressPtr + (fromIntegral remainingEnergy) + updatedBlockStatePtrOut + remainingEnergyOut + -- Process the and construct the outcome + newRemainingEnergy <- fromIntegral <$> FFI.peek remainingEnergyOut + status <- case statusCode of + 0 -> do + updatedBlockState <- FFI.peek updatedBlockStatePtrOut >>= PLTBlockState.wrapFFIPtr + return $ + Right + ExecutionAccepts + { eaUpdatedPLTBlockState = updatedBlockState, + eaEvents = () + } + 1 -> return $ Left () + _ -> error "Unexpected status code from calling 'ffiExecuteTransaction'" + return + ExecutionOutcome + { erRemainingEnergy = newRemainingEnergy, + erStatus = status + } foreign import ccall "ffi_execute_transaction" ffiExecuteTransaction :: -- | Called to read data from blob store. FFI.LoadCallback -> + -- | Called to set the token account balance in the haskell-managed block state. + UpdateTokenAccountBalanceCallbackPtr -> -- | Pointer to the starting block state. FFI.Ptr PLTBlockState.PLTBlockState -> -- | Pointer to transaction payload bytes. @@ -118,3 +129,52 @@ data ExecutionAccepts = ExecutionAccepts -- | Events produced during the execution eaEvents :: () } + +-- | Callback function for updating a token account balance. +type UpdateTokenAccountBalanceCallback = + -- | Index of the account to update a token balance for. + Types.AccountIndex -> + -- | Index of the token to update the balance of. + Tokens.TokenIndex -> + -- | The token amount to add to the balance. + Tokens.TokenRawAmount -> + -- | The token amount to subtract from the balance. + Tokens.TokenRawAmount -> + -- | Status code, where non-null represents a balance overflow. + IO Bool + +-- | Internal helper function for mapping the `UpdateTokenAccountBalanceCallback` into the more +-- low-level function pointer which can be passed in FFI. +wrapUpdateTokenAccountBalanceCallback :: UpdateTokenAccountBalanceCallback -> IO UpdateTokenAccountBalanceCallbackPtr +wrapUpdateTokenAccountBalanceCallback callback = + ffiWrapUpdateTokenAccountBalanceCallback $ wrapped + where + wrapped :: UpdateTokenAccountBalanceCallbackFFI + wrapped accountIndex tokenIndex add remove = do + overflow <- callback (fromIntegral accountIndex) (fromIntegral tokenIndex) (fromIntegral add) (fromIntegral remove) + return $ if overflow then 1 else 0 + +-- | Callback function for updating a token account balance. +-- +-- This is passed as a function pointer in FFI to call, see also `UpdateTokenAccountBalanceCallback` +-- for the more type-safe variant. +type UpdateTokenAccountBalanceCallbackFFI = + -- | Index of the account to update a token balance for. + Word.Word64 -> + -- | Index of the token to update the balance of. + Word.Word64 -> + -- | The token amount to add to the balance. + Word.Word64 -> + -- | The token amount to subtract from the balance. + Word.Word64 -> + -- | Status code, where non-null represents a balance overflow. + IO Word.Word8 + +-- | The callback function pointer type for updating a token account balance. +type UpdateTokenAccountBalanceCallbackPtr = FFI.FunPtr UpdateTokenAccountBalanceCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapUpdateTokenAccountBalanceCallback :: + UpdateTokenAccountBalanceCallbackFFI -> IO UpdateTokenAccountBalanceCallbackPtr diff --git a/plt-scheduler/src/block_state/ffi.rs b/plt-scheduler/src/block_state/ffi.rs index c3e12273a..9d2899da5 100644 --- a/plt-scheduler/src/block_state/ffi.rs +++ b/plt-scheduler/src/block_state/ffi.rs @@ -53,6 +53,7 @@ extern "C" fn ffi_empty_plt_block_state() -> *mut block_state::BlockStateSavepoi /// - Freeing is only ever done once. #[no_mangle] unsafe extern "C" fn ffi_free_plt_block_state(block_state: *mut block_state::BlockStateSavepoint) { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); let state = Box::from_raw(block_state); drop(state); } @@ -61,6 +62,7 @@ unsafe extern "C" fn ffi_free_plt_block_state(block_state: *mut block_state::Blo /// /// # Arguments /// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. /// - `block_state` Pointer to the PLT block state to compute a hash for. /// - `destination` Unique pointer with location to write the 32 bytes for the hash. /// @@ -73,6 +75,7 @@ unsafe extern "C" fn ffi_hash_plt_block_state( destination: *mut u8, block_state: *const block_state::BlockStateSavepoint, ) { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); let block_state = &*block_state; let hash = block_state.hash(load_callback); std::ptr::copy_nonoverlapping(hash.as_ptr(), destination, hash.len()); @@ -115,6 +118,7 @@ unsafe extern "C" fn ffi_store_plt_block_state( mut store_callback: StoreCallback, block_state: *mut block_state::BlockStateSavepoint, ) -> blob_store::Reference { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); let block_state = &mut *block_state; match block_state.store_update(&mut store_callback) { Ok(r) => r, @@ -142,6 +146,7 @@ unsafe extern "C" fn ffi_migrate_plt_block_state( mut store_callback: StoreCallback, block_state: *mut block_state::BlockStateSavepoint, ) -> *mut block_state::BlockStateSavepoint { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); let block_state = &mut *block_state; match block_state.migrate(&load_callback, &mut store_callback) { Ok(new_block_state) => Box::into_raw(Box::new(new_block_state)), @@ -164,10 +169,28 @@ unsafe extern "C" fn ffi_cache_plt_block_state( load_callback: LoadCallback, block_state: *mut block_state::BlockStateSavepoint, ) { + debug_assert!(!block_state.is_null(), "Block state is a null pointer."); let block_state = &mut *block_state; block_state.cache(&load_callback) } +/// External function for updating the token balance for an account. +/// +/// Returns non-zero if the balance overflows. +/// +/// # Arguments +/// +/// - `account_index` The index of the account to update a token balance for. +/// - `token_index` The index of the token. +/// - `add_amount` The amount to add to the balance. +/// - `remove_amount` The amount to subtract from the balance. +/// +/// # Safety +/// +/// Argument `account_index` must be a valid account index of an existing account. +pub type UpdateTokenAccountBalanceCallback = + extern "C" fn(account_index: u64, token_index: u64, add_amount: u64, remove_amount: u64) -> u8; + /// Runtime/execution state relevant for providing an implementation of /// [`crate::BlockStateOperations`]. /// @@ -180,6 +203,8 @@ pub(crate) struct ExecutionTimeBlockState { #[expect(dead_code)] /// External function for reading from the blob store. pub(crate) load_callback: LoadCallback, + /// External function for updating the token balance for an account. + pub(crate) update_token_account_balance_callback: UpdateTokenAccountBalanceCallback, } impl crate::BlockStateOperations for ExecutionTimeBlockState { @@ -227,11 +252,30 @@ impl crate::BlockStateOperations for ExecutionTimeBlockState { fn update_token_account_balance( &mut self, - _token_index: crate::TokenIndex, - _account_index: concordium_base::base::AccountIndex, - _amount_delta: crate::TokenAmountDelta, + token_index: crate::TokenIndex, + account_index: concordium_base::base::AccountIndex, + amount_delta: crate::TokenAmountDelta, ) -> Result<(), crate::OverflowError> { - todo!() + let (add_amount, remove_amount) = if amount_delta.is_negative() { + let remove_amount = + u64::try_from(amount_delta.abs()).map_err(|_| crate::OverflowError)?; + (0, remove_amount) + } else { + let add_amount = u64::try_from(amount_delta).map_err(|_| crate::OverflowError)?; + (add_amount, 0) + }; + + let overflow = (self.update_token_account_balance_callback)( + account_index.into(), + token_index.into(), + add_amount, + remove_amount, + ); + if overflow != 0 { + Err(crate::OverflowError) + } else { + Ok(()) + } } fn touch_token_account( diff --git a/plt-scheduler/src/ffi.rs b/plt-scheduler/src/ffi.rs index 5d928ba5c..7b1faef6c 100644 --- a/plt-scheduler/src/ffi.rs +++ b/plt-scheduler/src/ffi.rs @@ -15,6 +15,7 @@ use libc::size_t; /// # Arguments /// /// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `update_token_account_balance_callback` External function to call updating the token balance of an account. /// - `block_state` Unique pointer to a block state to mutate during execution. /// - `payload` Pointer to transaction payload bytes. /// - `payload_len` Byte length of transaction payload. @@ -32,6 +33,7 @@ use libc::size_t; #[no_mangle] unsafe extern "C" fn ffi_execute_transaction( load_callback: block_state::ffi::LoadCallback, + update_token_account_balance_callback: block_state::ffi::UpdateTokenAccountBalanceCallback, block_state: *const block_state::BlockStateSavepoint, payload: *const u8, payload_len: size_t, @@ -65,6 +67,7 @@ unsafe extern "C" fn ffi_execute_transaction( let mut block_state = block_state::ffi::ExecutionTimeBlockState { inner_block_state: (*block_state).new_generation(), load_callback, + update_token_account_balance_callback, }; let result = crate::execute_transaction(&mut scheduler_state, &mut block_state, payload); let block_state = block_state.inner_block_state; diff --git a/plt-scheduler/src/lib.rs b/plt-scheduler/src/lib.rs index 1c7b3b896..2b271e49b 100644 --- a/plt-scheduler/src/lib.rs +++ b/plt-scheduler/src/lib.rs @@ -11,7 +11,7 @@ mod ffi; pub type MutableTokenState = (); pub type PLTConfiguration = (); -pub type TokenAmountDelta = (); +pub type TokenAmountDelta = i128; /// Index of the protocol-level token in the block state map of tokens. #[derive(