diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 20ea47c249..9fb6521b7b 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -34,6 +34,7 @@ on: - 'concordium-consensus/smart-contracts' - 'concordium-consensus/haskell-lmdb' - 'concordium-node/rustfmt.toml' + - 'plt' pull_request: types: [opened, synchronize, reopened, ready_for_review] @@ -48,17 +49,19 @@ on: - 'concordium-consensus/smart-contracts' - 'concordium-consensus/haskell-lmdb' - 'concordium-node/rustfmt.toml' + - 'plt' workflow_dispatch: # allow manual trigger env: dummy: 17 # change to force cache invalidation CARGO_TERM_COLOR: always # implicitly adds '--color=always' to all cargo commands + RUST_VERSION: 1.86 + GHC_VERSION: 9.10.2 jobs: fourmolu: runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} steps: - name: Download fourmolu @@ -93,25 +96,17 @@ jobs: rustfmt: runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - - strategy: - matrix: - plan: - - rust: "1.82" - steps: - name: Checkout uses: actions/checkout@v2 with: - # token: ${{ secrets.CONCORDIUM_CI }} submodules: recursive - name: Install Rust uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: ${{ matrix.plan.rust }} + toolchain: ${{ env.RUST_VERSION }} override: true components: rustfmt @@ -127,13 +122,6 @@ jobs: needs: [fourmolu, rustfmt] # Use fixed OS version because we install packages on the system. runs-on: ubuntu-22.04 - if: ${{ !github.event.pull_request.draft }} - - strategy: - matrix: - plan: - - rust: 1.82 - ghc: 9.10.2 steps: - name: Remove unnecessary files @@ -164,7 +152,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: ${{ matrix.plan.rust }} + toolchain: ${{ env.RUST_VERSION }} override: true components: clippy, llvm-tools target: x86_64-pc-windows-gnu @@ -184,10 +172,10 @@ jobs: concordium-base/smart-contracts/lib concordium-node/target collector/target - key: ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ matrix.plan.rust }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}-${{ hashFiles('concordium-base/rust-src/**/*.rs','concordium-base/smart-contracts/wasm-chain-integration/**/*.rs')}} + key: ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ env.RUST_VERSION }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }}-${{ hashFiles('concordium-base/rust-src/**/*.rs','concordium-base/smart-contracts/wasm-chain-integration/**/*.rs')}} restore-keys: | - ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ matrix.plan.rust }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }} - ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ matrix.plan.rust }} + ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ env.RUST_VERSION }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }} + ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ env.RUST_VERSION }} # HASKELL # @@ -208,9 +196,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.stack - key: ${{ runner.os }}-${{ env.dummy }}-stack-global-${{ matrix.plan.ghc }}-${{ hashFiles('**.yaml') }} + key: ${{ runner.os }}-${{ env.dummy }}-stack-global-${{ env.GHC_VERSION }}-${{ hashFiles('**.yaml') }} restore-keys: | - ${{ runner.os }}-${{ env.dummy }}-stack-global-${{ matrix.plan.ghc }} + ${{ runner.os }}-${{ env.dummy }}-stack-global-${{ env.GHC_VERSION }} - name: Cache '.stack-work' uses: actions/cache@v4 with: @@ -220,11 +208,11 @@ jobs: concordium-consensus/.stack-work concordium-consensus/haskell-lmdb/.stack-work - key: ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ matrix.plan.ghc }}-${{ hashFiles('**.yaml') }}-${{ steps.cache-keys.outputs.proto_hash }}-${{ steps.cache-keys.outputs.base_hash }} + key: ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ env.GHC_VERSION }}-${{ hashFiles('**.yaml') }}-${{ steps.cache-keys.outputs.proto_hash }}-${{ steps.cache-keys.outputs.base_hash }} restore-keys: | - ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ matrix.plan.ghc }}-${{ hashFiles('**.yaml') }}-${{ steps.cache-keys.outputs.proto_hash }}- - ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ matrix.plan.ghc }}-${{ hashFiles('**.yaml') }}- - ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ matrix.plan.ghc }} + ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ env.GHC_VERSION }}-${{ hashFiles('**.yaml') }}-${{ steps.cache-keys.outputs.proto_hash }}- + ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ env.GHC_VERSION }}-${{ hashFiles('**.yaml') }}- + ${{ runner.os }}-${{ env.dummy }}-stack-work-${{ env.GHC_VERSION }} # Compile Haskell sources. This must be done before running checks or tests on the Rust sources. - name: Build consensus and run tests diff --git a/.github/workflows/deployment-build-test.yaml b/.github/workflows/deployment-build-test.yaml deleted file mode 100644 index 6f33b208b8..0000000000 --- a/.github/workflows/deployment-build-test.yaml +++ /dev/null @@ -1,78 +0,0 @@ -name: PLT Deployment unit checks - -on: - push: - branches: - - main - paths: - - '.github/workflows/deployment-build-test.yaml' - - 'concordium-base' - - 'deployment' - - pull_request: - paths: - - '.github/workflows/deployment-build-test.yaml' - - 'concordium-base' - - 'deployment' -env: - CARGO_TERM_COLOR: always # implicitly adds '--color=always' to all cargo commands - -jobs: - rustfmt: - name: Check formatting - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - submodules: recursive - - name: Run rustfmt - working-directory: plt-deployment-unit - run: | - rustup component add rustfmt - cargo fmt -- --check - - clippy_test: - name: Run Clippy and tests - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Clippy - working-directory: plt-deployment-unit - run: | - rustup component add clippy - cargo clippy --all-targets --all-features --locked -- -D warnings - - name: Test - working-directory: plt-deployment-unit - run: cargo test --all-targets --all-features - - # Build the deployment unit to WebAssembly and report the size in bytes - report_wasm_size: - name: Build and report byte size of Wasm deployment unit - runs-on: ubuntu-latest - needs: [rustfmt, clippy_test] - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Build to WebAssembly - working-directory: plt-deployment-unit - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cargo build --release --target wasm32-unknown-unknown --locked - - name: Report the byte size of WASM deployment unit on PRs - working-directory: plt-deployment-unit - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Read the size of the build artifact in number of bytes - BYTESIZE=$(du -b target/wasm32-unknown-unknown/release/plt_deployment_unit.wasm | egrep -o '^[0-9]+') - # Format the exact bytes. - GROUPED=$(numfmt --suffix B --grouping $BYTESIZE) - # Attach comment to PR. - gh pr comment ${{ github.event.number }} --edit-last --create-if-none --body "The file size of the WebAssembly PLT deployment unit is **$GROUPED**." diff --git a/.github/workflows/plt-build-test.yaml b/.github/workflows/plt-build-test.yaml new file mode 100644 index 0000000000..a19276f3c5 --- /dev/null +++ b/.github/workflows/plt-build-test.yaml @@ -0,0 +1,51 @@ +name: PLT scheduler build and run tests + +on: + push: + branches: + - main + paths: + - '.github/workflows/plt-build-test.yaml' + - 'concordium-base' + - 'plt' + + pull_request: + paths: + - '.github/workflows/plt-build-test.yaml' + - 'concordium-base' + - 'plt' +env: + CARGO_TERM_COLOR: always # implicitly adds '--color=always' to all cargo commands + +jobs: + rustfmt: + name: Check formatting + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + - name: Run rustfmt + working-directory: plt + run: | + rustup component add rustfmt + cargo fmt --check + + clippy_test: + name: Run Clippy and tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Clippy + working-directory: plt + run: | + rustup component add clippy + cargo clippy --locked --all-targets --all-features -- -D warnings + - name: Test + working-directory: plt + run: | + cargo test --locked --all-targets --all-features diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f6e4e3f170..a607d3e8ed 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ on: env: UBUNTU_VERSION: '22.04' STATIC_LIBRARIES_IMAGE_TAG: 'rust-1.82_ghc-9.10.2' - RUST_VERSION: '1.82' + RUST_VERSION: '1.86' STACK_VERSION: '3.7.1' FLATBUFFERS_VERSION: '23.5.26' GHC_VERSION: '9.10.2' diff --git a/.gitignore b/.gitignore index 0ccf8a9467..702fc77927 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ deps/internal/crypto/build *.ps scripts/static-libs/static-libs-docker/output_dir/ result -**/**/Cargo.lock 0 **/**/0 scripts/genesis-data/genesis_data/** @@ -49,6 +48,7 @@ genesis_data /concordium-consensus/a.out /concordium-consensus/HSdll.dll /concordium-consensus/HSdll.dll.a +/concordium-consensus/lib concordium-node/deps/static-libs .dir-locals.el hie.yaml diff --git a/collector/src/bin/collector.rs b/collector/src/bin/collector.rs index 1c36435bc4..23f2f229f7 100644 --- a/collector/src/bin/collector.rs +++ b/collector/src/bin/collector.rs @@ -11,7 +11,11 @@ use tonic::transport::{channel::Channel, ClientTlsConfig}; #[macro_use] extern crate log; -#[allow(clippy::large_enum_variant, clippy::enum_variant_names)] +#[allow( + clippy::large_enum_variant, + clippy::enum_variant_names, + clippy::doc_overindented_list_items +)] mod grpc { mod plt { tonic::include_proto!("concordium.v2.plt"); @@ -208,7 +212,7 @@ async fn main() { } #[allow(clippy::cognitive_complexity)] -async fn collect_data<'a>( +async fn collect_data( node_name: NodeName, grpc_host: String, conf: &ConfigCli, diff --git a/concordium-base b/concordium-base index 3b735e1012..d0590c6d67 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 3b735e1012411208ec20d7bb2b2d224239a21150 +Subproject commit d0590c6d67232d50342461075267b280c134daf2 diff --git a/concordium-consensus/Setup.hs b/concordium-consensus/Setup.hs index 640b57dca3..be3cba5cc6 100644 --- a/concordium-consensus/Setup.hs +++ b/concordium-consensus/Setup.hs @@ -4,52 +4,74 @@ import Distribution.Simple.LocalBuildInfo import Distribution.Simple.Setup import Distribution.Simple.Utils import Distribution.System +import Distribution.Verbosity import System.Directory import System.Environment import Data.Maybe -smartContractRoot = "../concordium-base/smart-contracts" +-- | Notify and execute a command, if fails exit with the same exit code. +runCmd :: Verbosity -> String -> IO () +runCmd verbosity cmd = do + notice verbosity $ "Running '" ++ cmd ++ "'" + let command : args = words cmd + rawSystemExit verbosity command $ args -makeRust :: Args -> ConfigFlags -> IO HookedBuildInfo -makeRust args flags = do +-- | Path to Concordium Smart Contract Engine rust crate relative to this file. +smartContractEngineCrateRelative = "../concordium-base/smart-contracts/wasm-chain-integration" + +-- | Path to plt rust workspace relative to this file. +pltWorkspaceRelative = "../plt" + +preConfHook :: Args -> ConfigFlags -> IO HookedBuildInfo +preConfHook args flags = do let verbosity = fromFlag $ configVerbosity flags - rawSystemExit verbosity "mkdir" ["-p", smartContractRoot ++ "/lib"] - -- This way of determining the platform is not ideal. - notice verbosity "Calling 'cargo build'" - rawSystemExit - verbosity - "cargo" - ["build", "--release", "--manifest-path", smartContractRoot ++ "/wasm-chain-integration/Cargo.toml", "--features=enable-ffi"] + -- Convert relative paths into absolute paths. + libraryDestination <- canonicalizePath "./lib" + -- Ensure destination directory exists. + runCmd verbosity $ "mkdir -p " ++ libraryDestination + + -- Build and copy/symlink Concordium Smart contract Engine library. + smartContractEngineCrate <- canonicalizePath smartContractEngineCrateRelative + runCmd verbosity $ "cargo build --release --locked --features=enable-ffi --manifest-path=" ++ smartContractEngineCrate ++ "/Cargo.toml" + case buildOS of + Windows -> do + runCmd verbosity $ "cp -u " ++ smartContractEngineCrate ++ "/target/release/concordium_smart_contract_engine.dll " ++ libraryDestination + OSX -> do + runCmd verbosity $ "ln -s -f " ++ smartContractEngineCrate ++ "/target/release/libconcordium_smart_contract_engine.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ smartContractEngineCrate ++ "/target/release/libconcordium_smart_contract_engine.dylib " ++ libraryDestination + _ -> do + runCmd verbosity $ "ln -s -f " ++ smartContractEngineCrate ++ "/target/release/libconcordium_smart_contract_engine.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ smartContractEngineCrate ++ "/target/release/libconcordium_smart_contract_engine.so " ++ libraryDestination + + -- Build and copy/symlink PLT scheduler project + pltWorkspace <- canonicalizePath pltWorkspaceRelative + runCmd verbosity $ "cargo build --release --locked --features ffi -p plt-scheduler --manifest-path=" ++ pltWorkspace ++ "/Cargo.toml" case buildOS of Windows -> do - notice verbosity "Copying concordium_smart_contract_engine library" - rawSystemExit verbosity "cp" ["-u", smartContractRoot ++ "/wasm-chain-integration/target/release/concordium_smart_contract_engine.dll", smartContractRoot ++ "/lib/"] - -- We remove the static library if it exists. Previously, it would have been copied - -- over, but now we want to just link with the dynamic library, so we ensure it is - -- removed. - rawSystemExit verbosity "rm" ["-f", smartContractRoot ++ "/lib/libconcordium_smart_contract_engine.a"] + runCmd verbosity $ "cp -u " ++ pltWorkspace ++ "/target/release/plt_scheduler.dll " ++ libraryDestination + OSX -> do + runCmd verbosity $ "ln -s -f " ++ pltWorkspace ++ "/target/release/libplt_scheduler.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ pltWorkspace ++ "/target/release/libplt_scheduler.dylib " ++ libraryDestination _ -> do - rawSystemExit verbosity "ln" ["-s", "-f", "../wasm-chain-integration/target/release/libconcordium_smart_contract_engine.a", smartContractRoot ++ "/lib/"] - case buildOS of - OSX -> - rawSystemExit verbosity "ln" ["-s", "-f", "../wasm-chain-integration/target/release/libconcordium_smart_contract_engine.dylib", smartContractRoot ++ "/lib/libconcordium_smart_contract_engine.dylib"] - _ -> - rawSystemExit verbosity "ln" ["-s", "-f", "../wasm-chain-integration/target/release/libconcordium_smart_contract_engine.so", smartContractRoot ++ "/lib/libconcordium_smart_contract_engine.so"] + runCmd verbosity $ "ln -s -f " ++ pltWorkspace ++ "/target/release/libplt_scheduler.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ pltWorkspace ++ "/target/release/libplt_scheduler.so " ++ libraryDestination return emptyHookedBuildInfo -- | On Windows, copy the DLL files to the binary install directory. This is to ensure that they -- are accessible when running the binaries, tests and benchmarks. -copyDlls :: Args -> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO () -copyDlls _ flags pkgDescr lbi = case buildOS of +postCopyHook :: Args -> CopyFlags -> PackageDescription -> LocalBuildInfo -> IO () +postCopyHook _ flags pkgDescr lbi = case buildOS of Windows -> do let installDirs = absoluteComponentInstallDirs pkgDescr lbi (localUnitId lbi) copydest - let copyLib lib = do - rawSystemExit verbosity "cp" ["-u", smartContractRoot ++ "/lib/" ++ lib ++ ".dll", bindir installDirs] - notice verbosity $ "Copy " ++ lib ++ " to " ++ bindir installDirs - copyLib "concordium_smart_contract_engine" + -- Copy DLL for Concordium Smart Contract Engine + smartContractEngineCrate <- canonicalizePath smartContractEngineCrateRelative + runCmd verbosity $ "cp -u " ++ smartContractEngineCrate ++ "/target/release/concordium_smart_contract_engine.dll " ++ bindir installDirs + -- Copy DLL for PLT scheduler + pltWorkspace <- canonicalizePath pltWorkspaceRelative + runCmd verbosity $ "cp -u " ++ pltWorkspace ++ "/target/release/plt_scheduler.dll " ++ bindir installDirs _ -> return () where distPref = fromFlag (copyDistPref flags) @@ -59,6 +81,6 @@ copyDlls _ flags pkgDescr lbi = case buildOS of main = defaultMainWithHooks $ simpleUserHooks - { preConf = makeRust, - postCopy = copyDlls + { preConf = preConfHook, + postCopy = postCopyHook } diff --git a/concordium-consensus/package.yaml b/concordium-consensus/package.yaml index 160d91acb3..b41873ded1 100644 --- a/concordium-consensus/package.yaml +++ b/concordium-consensus/package.yaml @@ -88,7 +88,9 @@ library: - -O2 - -fno-ignore-asserts - extra-libraries: concordium_smart_contract_engine + extra-libraries: + - concordium_smart_contract_engine + - plt_scheduler when: - condition: "!(os(windows)) && !(flag(dynamic))" diff --git a/concordium-consensus/src/Concordium/PLTScheduler.hs b/concordium-consensus/src/Concordium/PLTScheduler.hs new file mode 100644 index 0000000000..d94113711c --- /dev/null +++ b/concordium-consensus/src/Concordium/PLTScheduler.hs @@ -0,0 +1,41 @@ +{-# 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 ( + executeTransaction, +) where + +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 + +-- | 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 :: + -- | 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) + case statusCode of + 0 -> return $ Right () + 1 -> return $ Left () + _ -> error "Unexpected status code from calling 'ffiExecuteTransaction'" + +foreign import ccall "ffi_execute_transaction" + ffiExecuteTransaction :: + -- | Pointer to transaction payload bytes. + FFI.Ptr Word.Word8 -> + -- | Byte length of transaction payload. + FFI.CSize -> + -- | Status code + IO Word.Word8 diff --git a/concordium-consensus/stack.static.yaml b/concordium-consensus/stack.static.yaml index d9bb8acafe..a3f40505b1 100644 --- a/concordium-consensus/stack.static.yaml +++ b/concordium-consensus/stack.static.yaml @@ -9,7 +9,7 @@ extra-deps: extra-lib-dirs: - ../concordium-base/lib -- ../concordium-base/smart-contracts/lib +- ./lib ghc-options: # `simpl-tick-factor` parameter here is necessary due to a bug in the ghc: https://gitlab.haskell.org/ghc/ghc/-/issues/14637#note_413425 diff --git a/concordium-consensus/stack.yaml b/concordium-consensus/stack.yaml index e2f082d3ce..9d112e9a09 100644 --- a/concordium-consensus/stack.yaml +++ b/concordium-consensus/stack.yaml @@ -44,8 +44,8 @@ extra-deps: - ../concordium-base extra-lib-dirs: -- ../concordium-base/lib -- ../concordium-base/smart-contracts/lib + - ../concordium-base/lib + - ./lib # Override default flag values for local packages and extra-deps # flags: {} diff --git a/concordium-node/src/bin/bootstrap_checker.rs b/concordium-node/src/bin/bootstrap_checker.rs index 3bbfc4e4bb..0b7bfcb354 100644 --- a/concordium-node/src/bin/bootstrap_checker.rs +++ b/concordium-node/src/bin/bootstrap_checker.rs @@ -37,7 +37,7 @@ fn main() -> anyhow::Result<()> { let regenesis_arc: Arc = Arc::new(Regenesis::from_blocks(regenesis_blocks)); ensure!( - regenesis_arc.blocks.read().unwrap().len() > 0, + !regenesis_arc.blocks.read().unwrap().is_empty(), "Bootstrapper can't run without specifying genesis hashes." ); diff --git a/concordium-node/src/bin/bootstrapper.rs b/concordium-node/src/bin/bootstrapper.rs index abea67e83e..1f5e3f8cfa 100644 --- a/concordium-node/src/bin/bootstrapper.rs +++ b/concordium-node/src/bin/bootstrapper.rs @@ -40,7 +40,7 @@ async fn main() -> anyhow::Result<()> { let regenesis_arc: Arc = Arc::new(Regenesis::from_blocks(regenesis_blocks)); ensure!( - regenesis_arc.blocks.read().unwrap().len() > 0, + !regenesis_arc.blocks.read().unwrap().is_empty(), "Bootstrapper can't run without specifying genesis hashes." ); diff --git a/concordium-node/src/consensus_ffi/ffi.rs b/concordium-node/src/consensus_ffi/ffi.rs index 6c512b5dd0..b2a8e3b885 100644 --- a/concordium-node/src/consensus_ffi/ffi.rs +++ b/concordium-node/src/consensus_ffi/ffi.rs @@ -2383,7 +2383,7 @@ impl ConsensusContainer { use crate::grpc2::Require; let bhi = crate::grpc2::types::block_hash_input_to_ffi(block_hash).require()?; let (block_id_type, block_hash) = bhi.to_ptr(); - let token_id_len = token_id.value.as_bytes().len(); + let token_id_len = token_id.value.len(); if token_id_len > 255 { return Err(tonic::Status::invalid_argument( "TokenId: length must be at most 255 bytes", diff --git a/concordium-node/src/health.rs b/concordium-node/src/health.rs index 795e7646d9..abcada05fa 100644 --- a/concordium-node/src/health.rs +++ b/concordium-node/src/health.rs @@ -51,7 +51,7 @@ enum ServiceError<'a> { ServiceNotFound { service: &'a str }, } -impl<'a> ServiceError<'a> { +impl ServiceError<'_> { pub fn is_not_found(&self) -> bool { matches!(self, Self::ServiceNotFound { .. }) } diff --git a/plt-deployment-unit/Cargo.toml b/plt-deployment-unit/Cargo.toml deleted file mode 100644 index db6fcbe861..0000000000 --- a/plt-deployment-unit/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "plt-deployment-unit" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib", "staticlib", "rlib"] - -[dependencies] -concordium_base = {path = "../concordium-base/rust-src/concordium_base"} -# TODO Remove getrandom as dependency when possible, ideally as part of https://linear.app/concordium/issue/COR-2027 -getrandom = { version = "0.2", features = ["custom"]} - diff --git a/plt-deployment-unit/rust-toolchain.toml b/plt-deployment-unit/rust-toolchain.toml deleted file mode 100644 index 8b3841348c..0000000000 --- a/plt-deployment-unit/rust-toolchain.toml +++ /dev/null @@ -1,5 +0,0 @@ -[toolchain] -# Rust version 1.87 and newer produces WASM instructions from WASM V2 specification, -# which ATTOW are not supported by the Concordium Wasm engine. -channel = "1.86" -targets = [ "wasm32-unknown-unknown"] diff --git a/plt-deployment-unit/src/lib.rs b/plt-deployment-unit/src/lib.rs deleted file mode 100644 index cfa165a272..0000000000 --- a/plt-deployment-unit/src/lib.rs +++ /dev/null @@ -1,237 +0,0 @@ -use concordium_base::base::{AccountIndex, Energy}; -use concordium_base::contracts_common::AccountAddress; -use concordium_base::protocol_level_tokens::RawCbor; -use concordium_base::transactions::Memo; - -pub type StateKey = Vec; -pub type StateValue = Vec; -pub type TokenEventType = String; -pub type TokenEventDetails = RawCbor; -pub type Parameter = RawCbor; -pub type TokenRawAmount = u64; - -/// Operations provided by the deployment unit host. -/// -/// This is abstracted in a trait to allow for a testing stub. -pub trait HostOperations { - /// The type for the account object. - /// - /// The account is guaranteed to exist on chain, when holding an instance of this type. - type Account; - - /// Lookup the account using an account address. - fn account_by_address(&self, address: AccountAddress) -> Option; - - /// Lookup the account using an account index. - fn account_by_index(&self, index: AccountIndex) -> Option; - - /// Get the account index for the account. - fn account_index(&self, account: Self::Account) -> AccountIndex; - - /// Get the canonical account address of the account, i.e. the address used as part of the - /// credential deployment and not an alias. - fn account_canonical_address(&self, account: Self::Account) -> AccountAddress; - - /// Get the token balance of the account. - fn account_balance(&self, account: Self::Account) -> TokenRawAmount; - - /// Update the balance of the given account to zero if it didn't have a balance before. - /// - /// Returns `true` if the balance wasn't present on the given account and `false` otherwise. - fn touch(&mut self, account: Self::Account) -> bool; - - /// Mint a specified amount and deposit it in the account. - /// - /// # Events - /// - /// This will produce a `TokenMintEvent` in the logs. - /// - /// # Errors - /// - /// - [`AmountNotRepresentableError`] The total supply would exceed the representable amount. - fn mint( - &mut self, - account: Self::Account, - amount: TokenRawAmount, - ) -> Result<(), AmountNotRepresentableError>; - - /// Burn a specified amount from the account. - /// - /// # Events - /// - /// This will produce a `TokenBurnEvent` in the logs. - /// - /// # Errors - /// - /// - [`InsufficientBalanceError`] The sender has insufficient balance. - fn burn( - &mut self, - account: Self::Account, - amount: TokenRawAmount, - ) -> Result<(), InsufficientBalanceError>; - - /// Transfer a token amount from one account to another, with an optional memo. - /// - /// # Events - /// - /// This will produce a `TokenTransferEvent` in the logs. - /// - /// # Errors - /// - /// - [`InsufficientBalanceError`] The sender has insufficient balance. - fn transfer( - &mut self, - from: Self::Account, - to: Self::Account, - amount: TokenRawAmount, - memo: Option, - ) -> Result<(), InsufficientBalanceError>; - - /// The current token circulation supply. - fn circulating_supply(&self) -> TokenRawAmount; - - /// The number of decimals used in the presentation of the token amount. - fn decimals(&self) -> u8; - - /// Lookup a key in the token state. - fn get_token_state(&self, key: StateKey) -> Option; - - /// Set or clear a value in the token state at the corresponding key. - /// - /// Returns whether there was an existing entry. - /// - /// # Errors - /// - /// - [`LockedStateKeyError`] if the update failed because the key was locked by an iterator. - fn set_token_state( - &mut self, - key: StateKey, - value: Option, - ) -> Result; - - /// Reduce the available energy for the PLT module execution. - /// - /// If the available energy is smaller than the given amount, the containing transaction will - /// abort and the effects of the transaction will be rolled back. - /// The energy is charged in any case (also in case of failure). - fn tick_energy(&mut self, energy: Energy); - - /// Log a token module event with the specified type and details. - /// - /// # Events - /// - /// This will produce a `TokenModuleEvent` in the logs. - fn log_token_event(&mut self, event_type: TokenEventType, event_details: TokenEventDetails); -} - -/// The account has insufficient balance. -#[derive(Debug)] -pub struct InsufficientBalanceError; - -/// Update to state key failed because the key was locked by an iterator. -#[derive(Debug)] -pub struct LockedStateKeyError; - -/// Mint exceed the representable amount. -#[derive(Debug)] -pub struct AmountNotRepresentableError; - -/// Represents the reasons why [`initialize_token`] can fail. -#[derive(Debug)] -pub enum InitError {} -/// Represents the reasons why [`execute_token_update_transaction`] can fail. -#[derive(Debug)] -pub enum UpdateError {} -/// Represents the reasons why a query to the token module can fail. -#[derive(Debug)] -pub enum QueryError {} - -/// The context for a token-holder or token-governance transaction. -#[derive(Debug)] -pub struct TransactionContext { - /// The sender account object. - pub sender: Account, - /// The sender account address. This is the account alias that is used by the transaction itself. - pub sender_address: AccountAddress, -} - -/// Initialize a PLT by recording the relevant configuration parameters in the state and -/// (if necessary) minting the initial supply to the token governance account. -pub fn initialize_token( - _host: &mut impl HostOperations, - _token_parameter: Parameter, -) -> Result<(), InitError> { - todo!() -} - -/// Execute a token update transaction using the [`HostOperations`] implementation on `host` to -/// update state and produce events. -/// -/// When resulting in an `Err` signals a rejected operation and all of the calls to -/// [`HostOperations`] must be rolled back y the caller. -/// -/// The process is as follows: -/// -/// - Decode the transaction CBOR parameter. -/// - Check that amounts are within the representable range. -/// - For each transfer operation: -/// -/// - Check that the module is not paused. -/// - Check that the recipient is valid. -/// - Check allowList/denyList restrictions. -/// - Transfer the amount from the sender to the recipient, if the sender's balance is -/// sufficient. -/// -/// - For each list update operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module configuration allows the list operation. -/// - Check that the account to add/remove exists on-chain. -/// -/// - For each mint operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module is not paused. -/// - Check that the module configuration allows minting. -/// - Check that the minting process was successful. -/// -/// - For each burn operation: -/// -/// - Check that the governance account is the sender. -/// - Check that the module is not paused. -/// - Check that the module configuration allows burning. -/// - Check that the burning process was successful. -/// -/// - For each pause/unpause operation: -/// -/// - Check that the governance account is the sender. -/// -/// # INVARIANTS: -/// -/// - Token module state contains a correctly encoded governance account address. -pub fn execute_token_update_transaction( - _host: &mut Host, - _context: TransactionContext, - _token_parameter: Parameter, -) -> Result<(), UpdateError> -where - Host: HostOperations, -{ - todo!() -} - -/// Get the CBOR-encoded representation of the token module state. -pub fn query_token_module_state(_host: &impl HostOperations) -> Result { - todo!() -} - -/// Get the CBOR-encoded representation of the token module account state. -pub fn query_account_state( - _host: &Host, - _account: Host::Account, -) -> Result, QueryError> -where - Host: HostOperations, -{ - todo!() -} diff --git a/plt-deployment-unit/tests/host_stub.rs b/plt-deployment-unit/tests/host_stub.rs deleted file mode 100644 index d09c1fda20..0000000000 --- a/plt-deployment-unit/tests/host_stub.rs +++ /dev/null @@ -1,241 +0,0 @@ -use concordium_base::base::{AccountIndex, Energy}; -use concordium_base::contracts_common::AccountAddress; -use concordium_base::transactions::Memo; -use plt_deployment_unit::{ - AmountNotRepresentableError, HostOperations, InsufficientBalanceError, LockedStateKeyError, - StateKey, StateValue, TokenEventDetails, TokenEventType, -}; - -/// The deployment host stub providing an implementation of [`HostOperations`] and methods for -/// configuring the state of the host. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct HostStub { - /// List of accounts existing. - accounts: Vec, -} - -/// Internal representation of an Account in [`HostStub`]. -#[derive(Debug, Clone, PartialEq, Eq)] -struct Account { - /// The index of the account - index: AccountIndex, - /// The canonical account address of the account. - address: AccountAddress, - /// The token balance of the account. - balance: Option, -} - -impl HostStub { - /// Construct a new `HostStub` with a number of accounts. - /// - /// # Example - /// - /// ``` - /// let account_address0 = [0u8; 32]; - /// let account_address1 = [1u8; 32]; - /// let host = HostStub::with_accounts([(0, account_address0, None), (1, account_address1, Some(42))]); - /// assert!(host.account_by_address(account_address1).is_some(), "Account must exist"); - /// ``` - pub fn with_accounts( - accounts: impl IntoIterator)>, - ) -> Self { - let accounts = accounts - .into_iter() - .map(|(index, address, balance)| Account { - index, - address, - balance, - }) - .collect(); - - Self { accounts } - } -} - -/// Host stub account object. -/// -/// When testing it is the index into the list of accounts tracked by the `HostStub`. -/// Holding -#[derive(Debug, Clone, Copy)] -pub struct AccountStubIndex(usize); - -impl HostOperations for HostStub { - type Account = AccountStubIndex; - - fn account_by_address(&self, address: AccountAddress) -> Option { - self.accounts.iter().enumerate().find_map(|(i, account)| { - // TODO resolve an account alias as well here. - if account.address == address { - Some(AccountStubIndex(i)) - } else { - None - } - }) - } - - fn account_by_index(&self, index: AccountIndex) -> Option { - self.accounts.iter().enumerate().find_map(|(i, account)| { - if account.index == index { - Some(AccountStubIndex(i)) - } else { - None - } - }) - } - - fn account_index(&self, account: Self::Account) -> AccountIndex { - self.accounts[account.0].index - } - - fn account_canonical_address(&self, account: Self::Account) -> AccountAddress { - self.accounts[account.0].address - } - - fn account_balance(&self, account: Self::Account) -> u64 { - self.accounts[account.0].balance.unwrap_or(0) - } - - fn touch(&mut self, account: Self::Account) -> bool { - if self.accounts[account.0].balance.is_some() { - false - } else { - self.accounts[account.0].balance = Some(0); - true - } - } - - fn mint( - &mut self, - _account: Self::Account, - _amount: u64, - ) -> Result<(), AmountNotRepresentableError> { - todo!() - } - - fn burn( - &mut self, - _account: Self::Account, - _amount: u64, - ) -> Result<(), InsufficientBalanceError> { - todo!() - } - - fn transfer( - &mut self, - _from: Self::Account, - _to: Self::Account, - _amount: u64, - _memo: Option, - ) -> Result<(), InsufficientBalanceError> { - todo!() - } - - fn circulating_supply(&self) -> u64 { - todo!() - } - - fn decimals(&self) -> u8 { - todo!() - } - - fn get_token_state(&self, _key: StateKey) -> Option { - todo!() - } - - fn set_token_state( - &mut self, - _key: StateKey, - _value: Option, - ) -> Result { - todo!() - } - - fn tick_energy(&mut self, _energy: Energy) { - todo!() - } - - fn log_token_event(&mut self, _event_type: TokenEventType, _event_details: TokenEventDetails) { - todo!() - } -} - -// Tests for the HostStub - -const TEST_ACCOUNT0: AccountAddress = AccountAddress([0u8; 32]); -const TEST_ACCOUNT1: AccountAddress = AccountAddress([1u8; 32]); -const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); - -#[test] -fn test_account_lookup() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, None), - (1.into(), TEST_ACCOUNT1, None), - ]); - - let _ = host - .account_by_address(TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let _ = host - .account_by_address(TEST_ACCOUNT1) - .expect("Account is expected to exist"); - assert!( - host.account_by_address(TEST_ACCOUNT2).is_none(), - "Account is not expected to exist" - ); - // TODO test lookup using alias. - - let _ = host - .account_by_index(0.into()) - .expect("Account is expected to exist"); - let _ = host - .account_by_index(1.into()) - .expect("Account is expected to exist"); - assert!( - host.account_by_index(2.into()).is_none(), - "Account is not expected to exist" - ); -} - -#[test] -fn test_account_balance() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, Some(245)), - (1.into(), TEST_ACCOUNT1, None), - ]); - { - let account = host - .account_by_address(TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let balance = host.account_balance(account); - assert_eq!(balance, 245); - } - { - let account = host - .account_by_address(TEST_ACCOUNT1) - .expect("Account is expected to exist"); - let balance = host.account_balance(account); - assert_eq!(balance, 0); - } -} - -#[test] -fn test_account_canonical_address() { - let host = HostStub::with_accounts([ - (0.into(), TEST_ACCOUNT0, Some(245)), - (1.into(), TEST_ACCOUNT1, None), - ]); - { - let account = host - .account_by_address(TEST_ACCOUNT0) - .expect("Account is expected to exist"); - let balance = host.account_balance(account); - assert_eq!(balance, 245); - } - { - let account = host - .account_by_address(TEST_ACCOUNT1) - .expect("Account is expected to exist"); - let balance = host.account_balance(account); - assert_eq!(balance, 0); - } -} diff --git a/plt-deployment-unit/Cargo.lock b/plt/Cargo.lock similarity index 89% rename from plt-deployment-unit/Cargo.lock rename to plt/Cargo.lock index 989f338b8c..9da4daa040 100644 --- a/plt-deployment-unit/Cargo.lock +++ b/plt/Cargo.lock @@ -164,6 +164,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "autocfg" version = "1.5.0" @@ -184,9 +190,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bitvec" @@ -211,9 +217,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -221,15 +227,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -244,9 +250,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytecheck" @@ -278,15 +284,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "shlex", @@ -360,7 +366,7 @@ version = "4.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -381,7 +387,7 @@ dependencies = [ "concordium-contracts-common", "concordium_base_derive", "curve25519-dalek", - "derive_more", + "derive_more 0.99.20", "ed25519-dalek", "either", "ff", @@ -415,7 +421,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -487,9 +493,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -521,7 +527,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -555,7 +561,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -569,7 +575,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -580,7 +586,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -591,7 +597,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -635,7 +641,28 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.108", + "syn 2.0.111", +] + +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", ] [[package]] @@ -710,9 +737,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fnv" @@ -728,9 +755,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -795,9 +822,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hex" @@ -848,12 +875,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -878,15 +905,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -909,15 +936,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -1043,11 +1070,22 @@ dependencies = [ ] [[package]] -name = "plt-deployment-unit" +name = "plt-scheduler" version = "0.1.0" dependencies = [ "concordium_base", - "getrandom", + "derive_more 2.1.0", + "libc", + "plt-token-module", +] + +[[package]] +name = "plt-token-module" +version = "0.1.0" +dependencies = [ + "assert_matches", + "concordium_base", + "thiserror 2.0.17", ] [[package]] @@ -1105,9 +1143,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1185,7 +1223,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1259,9 +1297,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] name = "schemars" @@ -1277,9 +1315,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -1326,7 +1364,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1344,17 +1382,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros", @@ -1363,14 +1401,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.15.1" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1450,9 +1488,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1491,7 +1529,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1502,7 +1540,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1553,20 +1591,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime", "toml_parser", "winnow", @@ -1574,9 +1612,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -1589,9 +1627,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -1601,9 +1639,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", "wasm-bindgen", @@ -1623,9 +1661,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1636,9 +1674,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1646,22 +1684,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -1687,7 +1725,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1698,7 +1736,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1727,9 +1765,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -1745,22 +1783,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1780,5 +1818,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] diff --git a/plt/Cargo.toml b/plt/Cargo.toml new file mode 100644 index 0000000000..beed57c15d --- /dev/null +++ b/plt/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +resolver = "3" + +members = [ + "plt-scheduler", + "plt-token-module" +] + +[workspace.dependencies] +plt-token-module = {path = "plt-token-module"} + +concordium_base = {path = "../concordium-base/rust-src/concordium_base"} + +assert_matches = "1.5.0" +libc = "0.2.178" +thiserror = "2.0.17" diff --git a/plt/plt-scheduler/Cargo.toml b/plt/plt-scheduler/Cargo.toml new file mode 100644 index 0000000000..d792c8782f --- /dev/null +++ b/plt/plt-scheduler/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "plt-scheduler" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +[features] +ffi = ["dep:libc"] + +[dependencies] +concordium_base.workspace = true +plt-token-module.workspace = true +derive_more = { version = "2.1.0", features = ["into", "from"] } +libc = { workspace = true, optional = true } diff --git a/plt/plt-scheduler/README.md b/plt/plt-scheduler/README.md new file mode 100644 index 0000000000..c1c236b9b5 --- /dev/null +++ b/plt/plt-scheduler/README.md @@ -0,0 +1,8 @@ +# concordium-node: Scheduler implementation for Protocol-level token (PLT) + +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](https://github.com/Concordium/.github/blob/main/.github/CODE_OF_CONDUCT.md) +![Build and test](https://github.com/Concordium/concordium-node/actions/workflows/plt-scheduler-build-test.yaml/badge.svg) + +This crate provides a scheduler (transaction execution) implementation for concordium-node, and currently only supports transactions related to Protocol-level Tokens. + +It is compiled as a native library and is used within the Haskell implemented scheduler found as part of [`concordium-consensus`](../concordium-consensus/README.md). diff --git a/plt/plt-scheduler/src/block_state.rs b/plt/plt-scheduler/src/block_state.rs new file mode 100644 index 0000000000..bf74871613 --- /dev/null +++ b/plt/plt-scheduler/src/block_state.rs @@ -0,0 +1,76 @@ +use crate::BlockStateOperations; + +pub struct BlockState {} + +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() + } + + 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_token_module::token_kernel_interface::RawTokenAmount { + todo!() + } + + fn set_token_circulating_supply( + &mut self, + _token_index: crate::TokenIndex, + _circulating_supply: plt_token_module::token_kernel_interface::RawTokenAmount, + ) { + 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/plt-scheduler/src/ffi.rs b/plt/plt-scheduler/src/ffi.rs new file mode 100644 index 0000000000..9b30c13d2f --- /dev/null +++ b/plt/plt-scheduler/src/ffi.rs @@ -0,0 +1,54 @@ +//! This module provides a C ABI for this library. +//! It is only available if the `ffi` feature is enabled. + +use libc::size_t; + +/// C-binding for calling [`crate::execute_transaction`]. +/// +/// Returns a byte representing the status code, where the value should be interpreted as: +/// +/// - `0` execution succeeded. +/// - `1` rejected due to ... +/// +/// # Arguments +/// +/// - `payload` Pointer to transaction payload bytes. +/// - `payload_len` Byte length of transaction payload. +/// +/// # Safety +/// +/// - Argument `payload` must be non-null and valid for reads for `payload_len` many bytes. +#[unsafe(no_mangle)] +unsafe extern "C" fn ffi_execute_transaction(payload: *const u8, payload_len: size_t) -> u8 { + debug_assert!(!payload.is_null(), "Payload is a null pointer."); + let payload = unsafe { 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) { + Ok(()) => 0, + Err(crate::TransactionRejectReason) => 1, + } +} + +/// Trackes the energy remaining and some context during the execution. +struct SchedulerState {} +impl crate::SchedulerOperations for SchedulerState { + fn sender_account(&self) -> concordium_base::base::AccountIndex { + todo!() + } + + fn sender_account_address(&self) -> concordium_base::contracts_common::AccountAddress { + todo!() + } + + fn get_energy(&self) -> concordium_base::base::Energy { + todo!() + } + + fn tick_energy( + &mut self, + _energy: concordium_base::base::Energy, + ) -> Result<(), crate::OutOfEnergyError> { + todo!() + } +} diff --git a/plt/plt-scheduler/src/lib.rs b/plt/plt-scheduler/src/lib.rs new file mode 100644 index 0000000000..eb5e24222e --- /dev/null +++ b/plt/plt-scheduler/src/lib.rs @@ -0,0 +1,222 @@ +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::id::types::AccountAddress; +use concordium_base::protocol_level_tokens::TokenId; +use plt_token_module::token_kernel_interface::RawTokenAmount; + +mod block_state; +#[cfg(feature = "ffi")] +mod ffi; + +// Placeholder types to be defined or replaced with types from other crates. + +pub type MutableTokenState = (); +pub type PLTConfiguration = (); +pub type TokenAmountDelta = (); + +/// Index of the protocol-level token in the block state map of tokens. +#[derive( + Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::Into, derive_more::From, +)] +pub struct TokenIndex { + index: u64, +} + +/// Operations on the state of a block in the chain. +/// +/// This is abstracted in a trait to allow for a testing stub. +pub trait BlockStateOperations { + // Protocol-level token state query interface. + + /// Get the [`TokenId`]s of all protocol-level tokens registered on the chain. + /// + /// If the protocol version does not support protocol-level tokens, this will return the empty + /// list. + fn get_plt_list(&self) -> impl std::iter::Iterator; + + /// Get the [`TokenIndex`] associated with a [`TokenId`] (if it exists). + /// + /// # Arguments + /// + /// - `token_index` The token index to update. + fn get_token_index(&self, token_id: TokenId) -> Option; + + /// Convert a persistent state to a mutable one that can be updated by the scheduler. + /// + /// Updates to this state will only persist in the block state using [`BlockStateOperations::set_token_state`]. + /// + /// # Arguments + /// + /// - `token_index` The index of the token to get the state from. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn get_mutable_token_state(&self, token_index: TokenIndex) -> MutableTokenState; + + /// Get the configuration of a protocol-level token. + /// + /// # Arguments + /// + /// - `token_index` The index of the token to get the config for. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn get_token_configuration(&self, token_index: TokenIndex) -> PLTConfiguration; + + /// Get the circulating supply of a protocol-level token. + /// + /// # Arguments + /// + /// - `token_index` The index of the token to get the circulating supply. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn get_token_circulating_supply(&self, token_index: TokenIndex) -> RawTokenAmount; + + /// Set the recorded total circulating supply for a protocol-level token. + /// + /// This should always be kept up-to-date with the total balance held in accounts. + /// + /// # Arguments + /// + /// - `token_index` The token index to update. + /// - `circulation_supply` The new total circulating supply for the token. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn set_token_circulating_supply( + &mut self, + token_index: TokenIndex, + circulating_supply: RawTokenAmount, + ); + + /// Create a new token with the given configuration. The initial state will be empty + /// and the initial supply will be 0. Returns the token index and the updated state. + /// + /// # Arguments + /// + /// - `configuration` The configuration for the token. + /// + /// # Preconditions + /// + /// The caller must ensure the following conditions are true, and failing to do so results in + /// undefined behavior. + /// + /// - The `token_id` of the given configuration MUST NOT already be in use by a protocol-level + /// token, i.e. `assert_eq!(s.get_token_index(configuration.token_id), None)`. + /// - The [`PLTConfiguration`] MUST be valid and in particular the 'governance_account_index' + /// MUST reference a valid account. + fn create_token(&mut self, configuration: PLTConfiguration) -> TokenIndex; + + /// Update the token balance of an account. + /// + /// # Arguments + /// + /// - `token_index` The token index to update. + /// - `account_index` The account to update. + /// - `amount_delta` The token balance delta. + /// + /// # Errors + /// + /// - [`OverflowError`] The update would overflow or underflow the token balance on the account. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn update_token_account_balance( + &mut self, + token_index: TokenIndex, + account_index: AccountIndex, + amount_delta: TokenAmountDelta, + ) -> Result<(), OverflowError>; + + /// Touch the token account. This initializes a token account state with a + /// balance of zero. This only affects an account if its state for the token + /// is empty. + /// + /// Returns `false`, if the account already contained a token account state. + /// + /// # Arguments + /// + /// - `token_index` The token index to update. + /// - `account_index` The account to update. + /// + /// # Panics + /// + /// Panics if: + /// + /// - the token identified by `token_index` does not exist. + /// - the account identified by `account_index` does not exist. + #[must_use] + fn touch_token_account(&mut self, token_index: TokenIndex, account_index: AccountIndex) + -> bool; + + /// Increment the update sequence number for Protocol Level Tokens (PLT). + /// + /// Unlike the other chain updates this is a separate function, since there is no queue associated with PLTs. + fn increment_plt_update_sequence_number(&mut self); + + /// Convert a mutable state to a persistent one and store it in the block state. + /// + /// To ensure this is future-proof, the mutable state should not be used after this call. + /// + /// # Arguments + /// + /// - `token_index` The token index to update. + /// - `mutable_token_state` The mutated state to set as the current token state. + /// + /// # Panics + /// + /// Panics if the token identified by `token_index` does not exist. + fn set_token_state(&mut self, token_index: TokenIndex, mutable_token_state: MutableTokenState); +} + +/// Operations on the scheduler state. +pub trait SchedulerOperations { + /// The account initiating the transaction. + fn sender_account(&self) -> AccountIndex; + + /// The address of the account initiating the transaction. + fn sender_account_address(&self) -> AccountAddress; + + /// Get the amount of energy remaining for the execution. + fn get_energy(&self) -> Energy; + + /// Reduce the available energy for the execution. + /// + /// # Arguments + /// + /// - `energy` The amount of energy to charge. + /// + /// # Errors + /// + /// - [`OutOfEnergyError`] If the available energy is smaller than the ticked amount. + fn tick_energy(&mut self, energy: Energy) -> Result<(), OutOfEnergyError>; +} + +/// Transaction execution ran out of energy. +#[derive(Debug)] +pub struct OutOfEnergyError; + +/// The computation resulted in overflow. +#[derive(Debug)] +pub struct OverflowError; + +pub struct TransactionRejectReason; + +pub type Events = (); + +/// Execute a transaction payload modifying `scheduler` and `block_state` accordingly. +/// Returns the events produce if successful otherwise a reject reason. +/// +/// The caller must ensure to rollback state changes in case of the transaction being rejected. +pub fn execute_transaction( + _scheduler: &mut impl SchedulerOperations, + _block_state: &mut impl BlockStateOperations, + _payload: &[u8], +) -> Result { + todo!() +} diff --git a/plt/plt-token-module/Cargo.toml b/plt/plt-token-module/Cargo.toml new file mode 100644 index 0000000000..68c1578dc0 --- /dev/null +++ b/plt/plt-token-module/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "plt-token-module" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +[dependencies] +concordium_base.workspace = true +thiserror.workspace = true + +[dev-dependencies] +assert_matches.workspace = true \ No newline at end of file diff --git a/plt-deployment-unit/README.md b/plt/plt-token-module/README.md similarity index 100% rename from plt-deployment-unit/README.md rename to plt/plt-token-module/README.md diff --git a/plt/plt-token-module/src/lib.rs b/plt/plt-token-module/src/lib.rs new file mode 100644 index 0000000000..2f56aa53ad --- /dev/null +++ b/plt/plt-token-module/src/lib.rs @@ -0,0 +1,2 @@ +pub mod token_kernel_interface; +pub mod token_module; diff --git a/plt/plt-token-module/src/token_kernel_interface.rs b/plt/plt-token-module/src/token_kernel_interface.rs new file mode 100644 index 0000000000..b575e5f42b --- /dev/null +++ b/plt/plt-token-module/src/token_kernel_interface.rs @@ -0,0 +1,151 @@ +//! Token kernel interface for protocol-level tokens. The kernel handles all operations affecting token +//! balance and supply and manages the state and events related to balances and supply. + +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::TokenModuleEventType; +use concordium_base::transactions::Memo; + +pub type StateKey = Vec; +pub type StateValue = Vec; + +/// Token amount without decimals specified. The token amount represented by +/// this type must always be represented with the number of decimals +/// the token natively has. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] +pub struct RawTokenAmount(pub u64); + +/// The account has insufficient balance. +#[derive(Debug)] +pub struct InsufficientBalanceError; + +/// Update to state key failed because the key was locked by an iterator. +#[derive(Debug, thiserror::Error)] +#[error("State key is locked")] +pub struct LockedStateKeyError; + +/// Mint exceed the representable amount. +#[derive(Debug, thiserror::Error)] +#[error("Amount not representable")] +pub struct AmountNotRepresentableError; + +/// Energy limit for execution reached. +#[derive(Debug, thiserror::Error)] +#[error("Out of energy")] +pub struct OutOfEnergyError; + +/// Queries provided by the token kernel. +pub trait TokenKernelQueries { + /// The type for the account object. + /// + /// The account is guaranteed to exist on chain, when holding an instance of this type. + type Account; + + /// Lookup the account using an account address. + fn account_by_address(&self, address: &AccountAddress) -> Option; + + /// Lookup the account using an account index. + fn account_by_index(&self, index: AccountIndex) -> Option; + + /// Get the account index for the account. + fn account_index(&self, account: &Self::Account) -> AccountIndex; + + /// Get the canonical account address of the account, i.e. the address used as part of the + /// credential deployment and not an alias. + fn account_canonical_address(&self, account: &Self::Account) -> AccountAddress; + + /// Get the token balance of the account. + fn account_balance(&self, account: &Self::Account) -> RawTokenAmount; + + /// The current token circulation supply. + fn circulating_supply(&self) -> RawTokenAmount; + + /// The number of decimals used in the presentation of the token amount. + fn decimals(&self) -> u8; + + /// Lookup a key in the token state. + fn get_token_state(&self, key: StateKey) -> Option; +} + +/// Operations provided by the token kernel. +pub trait TokenKernelOperations: TokenKernelQueries { + /// Update the balance of the given account to zero if it didn't have a balance before. + /// + /// Returns `true` if the balance wasn't present on the given account and `false` otherwise. + fn touch(&mut self, account: &Self::Account) -> bool; + + /// Mint a specified amount and deposit it in the account. + /// + /// # Events + /// + /// This will produce a `TokenMintEvent` in the logs. + /// + /// # Errors + /// + /// - [`AmountNotRepresentableError`] The total supply would exceed the representable amount. + fn mint( + &mut self, + account: &Self::Account, + amount: RawTokenAmount, + ) -> Result<(), AmountNotRepresentableError>; + + /// Burn a specified amount from the account. + /// + /// # Events + /// + /// This will produce a `TokenBurnEvent` in the logs. + /// + /// # Errors + /// + /// - [`InsufficientBalanceError`] The sender has insufficient balance. + fn burn( + &mut self, + account: &Self::Account, + amount: RawTokenAmount, + ) -> Result<(), InsufficientBalanceError>; + + /// Transfer a token amount from one account to another, with an optional memo. + /// + /// # Events + /// + /// This will produce a `TokenTransferEvent` in the logs. + /// + /// # Errors + /// + /// - [`InsufficientBalanceError`] The sender has insufficient balance. + fn transfer( + &mut self, + from: &Self::Account, + to: &Self::Account, + amount: RawTokenAmount, + memo: Option, + ) -> Result<(), InsufficientBalanceError>; + + /// Set or clear a value in the token state at the corresponding key. + /// + /// Returns whether there was an existing entry. + /// + /// # Errors + /// + /// - [`LockedStateKeyError`] if the update failed because the key was locked by an iterator. + fn set_token_state( + &mut self, + key: StateKey, + value: Option, + ) -> Result; + + /// Reduce the available energy for the PLT module execution. + /// + /// If the available energy is smaller than the given amount, an + /// "out of energy" error will be returned, in which case the caller + /// should stop execution and propagate the error upwards. + /// The energy is charged in any case (also in case of failure). + fn tick_energy(&mut self, energy: Energy) -> Result<(), OutOfEnergyError>; + + /// Log a token module event with the specified type and details. + /// + /// # Events + /// + /// This will produce a `TokenModuleEvent` in the logs. + fn log_token_event(&mut self, event: TokenModuleEventType); +} diff --git a/plt/plt-token-module/src/token_module.rs b/plt/plt-token-module/src/token_module.rs new file mode 100644 index 0000000000..3cfb39141e --- /dev/null +++ b/plt/plt-token-module/src/token_module.rs @@ -0,0 +1,253 @@ +//! Implementation of the protocol-level token module. +//! + +use crate::token_kernel_interface::*; +use concordium_base::common::cbor; +use concordium_base::common::cbor::{SerializationOptions, UnknownMapKeys, cbor_encode}; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::{ + RawCbor, TokenAmount, TokenModuleInitializationParameters, +}; + +/// Extension trait for `TokenKernelOperations` to provide convenience wrappers for +/// module state access and updating. +trait KernelOperationsExt: TokenKernelOperations { + /// Set or clear a value in the token module state at the corresponding key. + fn set_module_state<'a>( + &mut self, + key: impl IntoIterator, + value: Option, + ) -> Result<(), LockedStateKeyError> { + self.set_token_state(module_state_key(key), value)?; + Ok(()) + } +} + +impl KernelOperationsExt for T {} + +/// Little-endian prefix used to distinguish module state keys. +const MODULE_STATE_PREFIX: [u8; 2] = 0u16.to_le_bytes(); + +/// Construct a [`StateKey`] for a module key. This prefixes the key to +/// distinguish it from other keys. +fn module_state_key<'a>(key: impl IntoIterator) -> StateKey { + let iter = key.into_iter(); + let mut module_key = Vec::with_capacity(MODULE_STATE_PREFIX.len() + iter.size_hint().0); + module_key.extend_from_slice(&MODULE_STATE_PREFIX); + module_key.extend(iter); + module_key +} + +/// Represents the reasons why [`initialize_token`] can fail. +#[derive(Debug, thiserror::Error)] +pub enum TokenInitializationError { + #[error("Invalid token initialization parameters: {0}")] + InvalidInitializationParameters(String), + #[error("{0}")] + LockedStateKey(#[from] LockedStateKeyError), + #[error("The given governance account does not exist: {0}")] + GovernanceAccountDoesNotExist(AccountAddress), + #[error("The initial mint amount has wrong number of decimals: {0}")] + MintAmountDecimalsMismatch(#[from] TokenAmountDecimalsMismatchError), + #[error("The initial mint amount is not representable: {0}")] + MintAmountNotRepresentable(#[from] AmountNotRepresentableError), +} + +/// Represents the reasons why [`execute_token_update_transaction`] can fail. +#[derive(Debug, thiserror::Error)] +pub enum TokenUpdateError {} + +/// Represents the reasons why a query to the token module can fail. +#[derive(Debug)] +pub enum TokenQueryError {} + +/// The context for a token-holder or token-governance transaction. +#[derive(Debug)] +pub struct TransactionContext { + /// The sender account object. + pub sender: Account, + /// The sender account address. This is the account alias that is used by the transaction itself. + pub sender_address: AccountAddress, +} + +#[derive(Debug, thiserror::Error)] +#[error("Token amount decimals mismatch: expected {expected}, found {found}")] +pub struct TokenAmountDecimalsMismatchError { + pub expected: u8, + pub found: u8, +} + +/// Asserts that token amount has the right number of decimals and converts it to a plain +/// integer. +fn to_token_raw_amount( + amount: TokenAmount, + expected_decimals: u8, +) -> Result { + let decimals = amount.decimals(); + if decimals != expected_decimals { + return Err(TokenAmountDecimalsMismatchError { + expected: expected_decimals, + found: decimals, + }); + } + Ok(RawTokenAmount(amount.value())) +} + +const STATE_KEY_NAME: &[u8] = b"name"; +const STATE_KEY_METADATA: &[u8] = b"metadata"; +const STATE_KEY_ALLOW_LIST: &[u8] = b"allowList"; +const STATE_KEY_DENY_LIST: &[u8] = b"denyList"; +const STATE_KEY_MINTABLE: &[u8] = b"mintable"; +const STATE_KEY_BURNABLE: &[u8] = b"burnable"; +const STATE_KEY_GOVERNANCE_ACCOUNT: &[u8] = b"governanceAccount"; + +/// Initialize a PLT by recording the relevant configuration parameters in the state and +/// (if necessary) minting the initial supply to the token governance account. +pub fn initialize_token( + host: &mut impl TokenKernelOperations, + initialization_parameters_cbor: RawCbor, +) -> Result<(), TokenInitializationError> { + let decode_options = SerializationOptions { + unknown_map_keys: UnknownMapKeys::Fail, + }; + let init_params: TokenModuleInitializationParameters = + cbor::cbor_decode_with_options(initialization_parameters_cbor, decode_options).map_err( + |err| { + TokenInitializationError::InvalidInitializationParameters(format!( + "Error decoding token initialization parameters: {}", + err + )) + }, + )?; + if !init_params.additional.is_empty() { + return Err(TokenInitializationError::InvalidInitializationParameters( + format!( + "Unknown additional parameters: {}", + init_params + .additional + .keys() + .map(|k| k.as_str()) + .collect::>() + .join(", ") + ), + )); + } + let name = init_params.name.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token name is missing".to_string(), + ) + })?; + let metadata = init_params.metadata.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token metadata is missing".to_string(), + ) + })?; + let governance_account = init_params.governance_account.ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token governance account is missing".to_string(), + ) + })?; + host.set_module_state(STATE_KEY_NAME, Some(name.into()))?; + let encoded_metadata = cbor_encode(&metadata).map_err(|err| { + TokenInitializationError::InvalidInitializationParameters(format!( + "Error encoding token metadata: {}", + err + )) + })?; + host.set_module_state(STATE_KEY_METADATA, Some(encoded_metadata))?; + if init_params.allow_list == Some(true) { + host.set_module_state(STATE_KEY_ALLOW_LIST, Some(vec![]))?; + } + if init_params.deny_list == Some(true) { + host.set_module_state(STATE_KEY_DENY_LIST, Some(vec![]))?; + } + if init_params.mintable == Some(true) { + host.set_module_state(STATE_KEY_MINTABLE, Some(vec![]))?; + } + if init_params.burnable == Some(true) { + host.set_module_state(STATE_KEY_BURNABLE, Some(vec![]))?; + } + + let governance_account = host.account_by_address(&governance_account.address).ok_or( + TokenInitializationError::GovernanceAccountDoesNotExist(governance_account.address), + )?; + let governance_account_index = host.account_index(&governance_account); + host.set_module_state( + STATE_KEY_GOVERNANCE_ACCOUNT, + Some(governance_account_index.index.to_be_bytes().to_vec()), + )?; + if let Some(initial_supply) = init_params.initial_supply { + let mint_amount = to_token_raw_amount(initial_supply, host.decimals())?; + host.mint(&governance_account, mint_amount) + .map_err(TokenInitializationError::MintAmountNotRepresentable)?; + } + Ok(()) +} + +/// Execute a token update transaction using the [`TokenKernelOperations`] implementation on `host` to +/// update state and produce events. +/// +/// When resulting in an `Err` signals a rejected operation and all of the calls to +/// [`TokenKernelOperations`] must be rolled back y the caller. +/// +/// The process is as follows: +/// +/// - Decode the transaction CBOR parameter. +/// - Check that amounts are within the representable range. +/// - For each transfer operation: +/// +/// - Check that the module is not paused. +/// - Check that the recipient is valid. +/// - Check allowList/denyList restrictions. +/// - Transfer the amount from the sender to the recipient, if the sender's balance is +/// sufficient. +/// +/// - For each list update operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module configuration allows the list operation. +/// - Check that the account to add/remove exists on-chain. +/// +/// - For each mint operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows minting. +/// - Check that the minting process was successful. +/// +/// - For each burn operation: +/// +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows burning. +/// - Check that the burning process was successful. +/// +/// - For each pause/unpause operation: +/// +/// - Check that the governance account is the sender. +/// +/// # INVARIANTS: +/// +/// - Token module state contains a correctly encoded governance account address. +pub fn execute_token_update_transaction( + _kernel: &mut Kernel, + _context: TransactionContext, + _token_operations: RawCbor, +) -> Result<(), TokenUpdateError> { + todo!() +} + +/// Get the CBOR-encoded representation of the token module state. +pub fn query_token_module_state( + _kernel: &impl TokenKernelQueries, +) -> Result { + todo!() +} + +/// Get the CBOR-encoded representation of the token module account state. +pub fn query_account_state( + _kernel: &impl TokenKernelQueries, + _account: Kernel::Account, +) -> Result, TokenQueryError> { + todo!() +} diff --git a/plt/plt-token-module/tests/kernel_stub.rs b/plt/plt-token-module/tests/kernel_stub.rs new file mode 100644 index 0000000000..753fbb5084 --- /dev/null +++ b/plt/plt-token-module/tests/kernel_stub.rs @@ -0,0 +1,257 @@ +use std::collections::HashMap; + +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::TokenModuleEventType; +use concordium_base::transactions::Memo; +use plt_token_module::token_kernel_interface::{ + AmountNotRepresentableError, InsufficientBalanceError, LockedStateKeyError, RawTokenAmount, + StateKey, StateValue, TokenKernelOperations, TokenKernelQueries, +}; + +/// Token kernel stub providing an implementation of [`TokenKernelOperations`] and methods for +/// configuring the state of the host. +#[derive(Debug)] +pub struct KernelStub { + /// List of accounts existing. + accounts: Vec, + /// Token managed state. + pub state: HashMap, + /// Decimal places in token representation. + decimals: u8, + next_account_index: AccountIndex, +} + +/// Internal representation of an Account in [`KernelStub`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Account { + /// The index of the account + pub index: AccountIndex, + /// The canonical account address of the account. + pub address: AccountAddress, + /// The token balance of the account. + pub balance: Option, +} + +impl KernelStub { + /// Create + pub fn new(decimals: u8) -> Self { + Self { + accounts: vec![], + state: Default::default(), + decimals, + next_account_index: AccountIndex { index: 0 }, + } + } + + /// Create an account in the stub. + /// + /// # Example + /// + /// ``` + /// let mut stub = KernelStub::new(0); + /// let account = stub.create_account(); + /// assert!(host.account_by_address(account_address1).is_some(), "Account must exist"); + /// ``` + pub fn create_account(&mut self) -> AccountStubIndex { + let index = self.next_account_index; + let mut address = AccountAddress([0u8; 32]); + address.0[..8].copy_from_slice(&index.index.to_be_bytes()); + let account = Account { + index, + address, + balance: None, + }; + let stub_index = AccountStubIndex(self.accounts.len()); + self.accounts.push(account); + + self.next_account_index.index += 1; + + stub_index + } + + /// Set account balance in the stub + pub fn set_account_balance(&mut self, account: AccountStubIndex, balance: RawTokenAmount) { + self.accounts + .get_mut(account.0) + .expect("account in stub") + .balance = Some(balance); + } +} + +/// Host stub account object. +/// +/// When testing it is the index into the list of accounts tracked by the `KernelStub`. +/// Holding +#[derive(Debug, Clone, Copy)] +pub struct AccountStubIndex(usize); + +impl TokenKernelQueries for KernelStub { + type Account = AccountStubIndex; + + fn account_by_address(&self, address: &AccountAddress) -> Option { + self.accounts.iter().enumerate().find_map(|(i, account)| { + // TODO resolve an account alias as well here. + if account.address == *address { + Some(AccountStubIndex(i)) + } else { + None + } + }) + } + + fn account_by_index(&self, index: AccountIndex) -> Option { + self.accounts.iter().enumerate().find_map(|(i, account)| { + if account.index == index { + Some(AccountStubIndex(i)) + } else { + None + } + }) + } + + fn account_index(&self, account: &Self::Account) -> AccountIndex { + self.accounts[account.0].index + } + + fn account_canonical_address(&self, account: &Self::Account) -> AccountAddress { + self.accounts[account.0].address + } + + fn account_balance(&self, account: &Self::Account) -> RawTokenAmount { + self.accounts[account.0].balance.unwrap_or_default() + } + + fn circulating_supply(&self) -> RawTokenAmount { + todo!() + } + + fn decimals(&self) -> u8 { + self.decimals + } + + fn get_token_state(&self, key: StateKey) -> Option { + self.state.get(&key).cloned() + } +} + +impl TokenKernelOperations for KernelStub { + fn touch(&mut self, account: &Self::Account) -> bool { + if self.accounts[account.0].balance.is_some() { + false + } else { + self.accounts[account.0].balance = Some(RawTokenAmount::default()); + true + } + } + + fn mint( + &mut self, + account: &Self::Account, + amount: RawTokenAmount, + ) -> Result<(), AmountNotRepresentableError> { + if let Some(balance) = self.accounts[account.0].balance { + if balance > RawTokenAmount(u64::MAX - amount.0) { + Err(AmountNotRepresentableError) + } else { + self.accounts[account.0].balance = Some(RawTokenAmount(balance.0 + amount.0)); + Ok(()) + } + } else { + self.accounts[account.0].balance = Some(amount); + Ok(()) + } + } + + fn burn( + &mut self, + _account: &Self::Account, + _amount: RawTokenAmount, + ) -> Result<(), InsufficientBalanceError> { + todo!() + } + + fn transfer( + &mut self, + _from: &Self::Account, + _to: &Self::Account, + _amount: RawTokenAmount, + _memo: Option, + ) -> Result<(), InsufficientBalanceError> { + todo!() + } + + fn set_token_state( + &mut self, + key: StateKey, + value: Option, + ) -> Result { + let res = match value { + None => self.state.remove(&key).is_some(), + Some(value) => self.state.insert(key, value).is_some(), + }; + Ok(res) + } + + fn tick_energy(&mut self, _energy: Energy) { + todo!() + } + + fn log_token_event(&mut self, _event: TokenModuleEventType) { + todo!() + } +} + +// Tests for the kernel stub + +const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); + +/// Test lookup account address and account from address +#[test] +fn test_account_lookup_address() { + let mut stub = KernelStub::new(0); + let account = stub.create_account(); + + let address = stub.account_canonical_address(&account); + stub.account_by_address(&address) + .expect("Account is expected to exist"); + assert!( + stub.account_by_address(&TEST_ACCOUNT2).is_none(), + "Account is not expected to exist" + ); +} + +/// Test lookup account index and account from index +#[test] +fn test_account_lookup_index() { + let mut stub = KernelStub::new(0); + let account = stub.create_account(); + + let index = stub.account_index(&account); + stub.account_by_index(index) + .expect("Account is expected to exist"); + assert!( + stub.account_by_index(2.into()).is_none(), + "Account is not expected to exist" + ); +} + +/// Test get account balance +#[test] +fn test_account_balance() { + let mut stub = KernelStub::new(0); + let account0 = stub.create_account(); + let account1 = stub.create_account(); + stub.set_account_balance(account0, RawTokenAmount(245)); + + let balance = stub.account_balance(&account0); + assert_eq!(balance, RawTokenAmount(245)); + + let balance = stub.account_balance(&account1); + assert_eq!(balance, RawTokenAmount(0)); +} + +#[test] +fn test_account_lookup_canonical_address() { + // TODO test lookup using alias. +} diff --git a/plt/plt-token-module/tests/token_module_initialize.rs b/plt/plt-token-module/tests/token_module_initialize.rs new file mode 100644 index 0000000000..fc4cf8e7bb --- /dev/null +++ b/plt/plt-token-module/tests/token_module_initialize.rs @@ -0,0 +1,268 @@ +use std::collections::HashMap; + +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::{ + common::cbor::value::Value, + protocol_level_tokens::{TokenAmount, TokenModuleInitializationParameters}, +}; +use kernel_stub::KernelStub; +use plt_token_module::token_kernel_interface::{RawTokenAmount, TokenKernelQueries}; +use plt_token_module::token_module::{ + self, TokenAmountDecimalsMismatchError, TokenInitializationError, +}; + +mod kernel_stub; + +const TEST_ACCOUNT2: AccountAddress = AccountAddress([2u8; 32]); + +/// In this example, the parameters are not a valid encoding. +#[test] +fn test_initialize_token_parameters_decode_failure() { + let mut stub = KernelStub::new(0); + let res = token_module::initialize_token(&mut stub, vec![].into()); + assert_matches!( + &res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err.contains("Error decoding token initialization parameters") + ); +} + +/// In this example, a parameter is missing from the required initialization parameters. +#[test] +fn test_initialize_token_parameters_missing() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let parameters = TokenModuleInitializationParameters { + name: None, + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!(res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err == "Token name is missing" + ); +} + +/// In this example, an unsupported additional parameter is present in the +/// initialization parameters. +#[test] +fn test_initialize_token_additional_parameter() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let mut additional = HashMap::with_capacity(1); + additional.insert("_param1".into(), Value::Text("extravalue1".into())); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional, + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::InvalidInitializationParameters(err)) + if err == "Unknown additional parameters: _param1" + ); +} + +/// In this example, minimal parameters are specified to check defaulting +/// behaviour. +#[test] +fn test_initialize_token_default_values() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters are valid, no minting. +#[test] +fn test_initialize_token_no_minting() { + let mut stub = KernelStub::new(0); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + expected_state.insert(b"\0\0allowList".into(), vec![]); + expected_state.insert(b"\0\0mintable".into(), vec![]); + expected_state.insert(b"\0\0burnable".into(), vec![]); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters are valid, with minting. +#[test] +fn test_initialize_token_valid_2() { + let mut stub = KernelStub::new(2); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let encoded_metadata = cbor::cbor_encode(&metadata).unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(true), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + token_module::initialize_token(&mut stub, encoded_parameters).unwrap(); + assert_eq!(stub.account_balance(&gov_account), RawTokenAmount(500000)); + let mut expected_state = HashMap::with_capacity(3); + expected_state.insert(b"\0\0name".into(), b"Protocol-level token".into()); + expected_state.insert(b"\0\0metadata".into(), encoded_metadata); + expected_state.insert( + b"\0\0governanceAccount".into(), + stub.account_index(&gov_account).index.to_be_bytes().into(), + ); + expected_state.insert(b"\0\0denyList".into(), vec![]); + assert_eq!(stub.state, expected_state); +} + +/// In this example, the parameters specify an initial supply with higher precision +/// than the token allows. +#[test] +fn test_initialize_token_excessive_mint_decimals() { + let mut stub = KernelStub::new(2); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 6)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::MintAmountDecimalsMismatch( + TokenAmountDecimalsMismatchError { + expected: 2, + found: 6 + } + )) + ); +} + +/// In this example, the parameters specify an initial supply with less precision +/// than the token allows. +#[test] +fn test_initialize_token_insufficient_mint_decimals() { + let mut stub = KernelStub::new(6); + let gov_account = stub.create_account(); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(stub.account_canonical_address(&gov_account).into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::MintAmountDecimalsMismatch( + TokenAmountDecimalsMismatchError { + expected: 6, + found: 2 + } + )) + ); +} + +/// In this example, the parameters specify an initial supply with less precision +/// than the token allows. +#[test] +fn test_initialize_token_non_existing_governance_account() { + let mut stub = KernelStub::new(0); + let metadata = "https://plt.token".to_owned().into(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata), + governance_account: Some(TEST_ACCOUNT2.into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + additional: Default::default(), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).unwrap().into(); + let res = token_module::initialize_token(&mut stub, encoded_parameters); + assert_matches!( + res, + Err(TokenInitializationError::GovernanceAccountDoesNotExist( + TEST_ACCOUNT2 + )) + ); +} diff --git a/plt/rust-toolchain.toml b/plt/rust-toolchain.toml new file mode 100644 index 0000000000..e7f22fb8ba --- /dev/null +++ b/plt/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.86" diff --git a/scripts/distribution/windows/build-all.ps1 b/scripts/distribution/windows/build-all.ps1 index 9d87bc4aff..4e28bd0143 100644 --- a/scripts/distribution/windows/build-all.ps1 +++ b/scripts/distribution/windows/build-all.ps1 @@ -59,8 +59,9 @@ try { $filesToSign = @( "$StackInstallRoot\lib\concordium-consensus.dll", "..\..\..\concordium-base\lib\concordium_base.dll", - "..\..\..\concordium-base\smart-contracts\lib\concordium_smart_contract_engine.dll", "..\..\..\concordium-base\lib\sha_2.dll", + "..\..\..\concordium-consensus\lib\concordium_smart_contract_engine.dll", + "..\..\..\concordium-consensus\lib\plt_scheduler.dll", "..\..\..\service\windows\target\x86_64-pc-windows-msvc\release\node-runner-service.exe", "..\..\..\collector\target\release\node-collector.exe", "..\..\..\concordium-node\target\release\concordium-node.exe" diff --git a/service/windows/src/main.rs b/service/windows/src/main.rs index 8e7bd254ea..1eafc77c20 100644 --- a/service/windows/src/main.rs +++ b/service/windows/src/main.rs @@ -49,9 +49,6 @@ fn runner_service_main(arguments: Vec) { } } -/// Macro for constructing a simple status message given the new state and -/// enabled controls (if any). - /// Construct a simple status message with no enabled controls. fn simple_status(state: ServiceState) -> ServiceStatus { simple_status_with_controls(state, ServiceControlAccept::empty()) diff --git a/stack.yaml b/stack.yaml index a9aac9301c..9c0fdc4545 100644 --- a/stack.yaml +++ b/stack.yaml @@ -12,4 +12,4 @@ extra-deps: extra-lib-dirs: - ./concordium-base/lib -- ./concordium-base/smart-contracts/lib +- ./concordium-consensus/lib