diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index f5f2d2db78..5b9ceef050 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -53,12 +53,12 @@ on: env: dummy: 17 # change to force cache invalidation CARGO_TERM_COLOR: always # implicitly adds '--color=always' to all cargo commands + GHC_VERSION: 9.10.2 jobs: fourmolu: runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} steps: - name: Download fourmolu @@ -93,30 +93,15 @@ jobs: rustfmt: runs-on: ubuntu-latest - if: ${{ !github.event.pull_request.draft }} - - strategy: - matrix: - plan: - - rust: "1.94" - 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 }} - override: true - components: rustfmt - - name: Run rustfmt run: | + rustup component add rustfmt cargo fmt --manifest-path concordium-node/Cargo.toml -- --check cargo fmt --manifest-path collector-backend/Cargo.toml -- --check cargo fmt --manifest-path collector/Cargo.toml -- --check @@ -127,13 +112,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.94 - ghc: 9.10.2 steps: - name: Remove unnecessary files @@ -160,19 +138,19 @@ jobs: # This must be done before checking the Rust sources (obviously) # but also before building the Haskell sources because the Haskell # build kicks of a Rust build. - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ matrix.plan.rust }} - override: true - components: clippy, llvm-tools - target: x86_64-pc-windows-gnu - # Install tool for code coverage reporting. - # Version cargo-llvm-cov 0.6.22 requries Rust 1.87, so we use 0.6.21 for now - - name: Install cargo-llvm-cov + - name: Install Rust tools + run: | + rustup component add clippy llvm-tools + rustup target add x86_64-pc-windows-gnu + cargo install cargo-llvm-cov --locked + - name: Read Rust version + id: read-rust-version + run: | + echo "RUST_VERSION=$(cargo --version | awk '{print $2}')" >> $GITHUB_OUTPUT + - name: Print Rust version + id: print-rust-version run: | - cargo install cargo-llvm-cov --version 0.6.21 --locked + echo "Rust version: ${{ steps.read-rust-version.outputs.RUST_VERSION }}" - name: Cache cargo dependencies and targets uses: actions/cache@v4 with: @@ -185,10 +163,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-${{ steps.read-rust-version.outputs.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-${{ steps.read-rust-version.outputs.RUST_VERSION }}-${{ hashFiles('**/Cargo.toml', '**/Cargo.lock') }} + ${{ runner.os }}-${{ env.dummy }}-rust-deps-${{ steps.read-rust-version.outputs.RUST_VERSION }} # HASKELL # @@ -209,9 +187,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: @@ -221,11 +199,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 (with code coverage) 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..cbb6bad0c8 --- /dev/null +++ b/.github/workflows/plt-build-test.yaml @@ -0,0 +1,69 @@ +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: Install tools + working-directory: plt + run: | + rustup component add clippy llvm-tools + cargo install cargo-llvm-cov --locked + - name: Clippy + working-directory: plt + run: | + cargo clippy --locked --all-targets --all-features -- -D warnings + - name: Test + working-directory: plt + run: | + cargo llvm-cov test --locked --all-targets --all-features --no-report + - name: Generate a report of the tests above. + working-directory: plt + run: | + cargo llvm-cov report --lcov --output-path plt_lcov.info + # Options documented here: https://github.com/codecov/codecov-action + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + disable_telem: true + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + files: plt/plt_lcov.info diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d67fe355cc..fbb5f901e2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,8 +21,7 @@ on: env: UBUNTU_VERSION: '22.04' - STATIC_LIBRARIES_IMAGE_TAG: 'rust-1.94_ghc-9.10.2' - RUST_VERSION: '1.94.0' + STATIC_LIBRARIES_IMAGE_TAG: 'rust-1.95.0_ghc-9.10.2' STACK_VERSION: '3.7.1' FLATBUFFERS_VERSION: '23.5.26' GHC_VERSION: '9.10.2' @@ -119,7 +118,6 @@ jobs: ghc_version=${{ env.GHC_VERSION }} protoc_version=${{ env.PROTOC_VERSION }} flatbuffers_version=${{ env.FLATBUFFERS_VERSION }} - rust_toolchain_version=${{ env.RUST_VERSION }} labels: | ubuntu_version=${{ env.UBUNTU_VERSION }} static_libraries_image_tag=${{ env.STATIC_LIBRARIES_IMAGE_TAG }} @@ -306,17 +304,16 @@ jobs: smctl healthcheck --all shell: cmd - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: ${{ env.RUST_VERSION }}-x86_64-pc-windows-msvc - rustflags: "" + - name: Read Rust version + id: read-rust-version + run: | + $rustVersion = (cargo --version).Split(" ")[1] + echo "RUST_VERSION=$rustVersion" >> $env:GITHUB_OUTPUT - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: ${{ env.RUST_VERSION }}-x86_64-pc-windows-gnu - rustflags: "" + - name: Print Rust version + id: print-rust-version + run: | + echo "Rust version: ${{ steps.read-rust-version.outputs.RUST_VERSION }}" - name: Setup node folder run: | @@ -362,7 +359,7 @@ jobs: SM_CLIENT_CERT_PASSWORD: ${{ secrets.WINDOWS_SM_CLIENT_CERT_PASSWORD }} SM_ARGS: "--verbose --exit-non-zero-on-fail --failfast" run: | - ./scripts/distribution/windows/build-all.ps1 -nodeVersion ${{ needs.validate-preconditions.outputs.version }} -rustVersion ${{ env.RUST_VERSION }} + ./scripts/distribution/windows/build-all.ps1 -nodeVersion ${{ needs.validate-preconditions.outputs.version }} -rustVersion ${{ steps.read-rust-version.outputs.RUST_VERSION }} - name: Sign installer with smctl working-directory: ${{steps.build.outputs.bin_dir}} @@ -435,11 +432,6 @@ jobs: security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security list-keychain -d user -s $KEYCHAIN_PATH - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: ${{ env.RUST_VERSION }} - rustflags: "" - - uses: haskell-actions/setup@v2 with: ghc-version: ${{ env.GHC_VERSION }} diff --git a/.gitignore b/.gitignore index b4e95ca100..14a9c34c38 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ concordium-node/.idea/ .so .stack-work .stack-work-test +.stack-work-bench dist .cabal-sandbox cabal.sandbox.config @@ -27,7 +28,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 +49,8 @@ genesis_data /concordium-consensus/a.out /concordium-consensus/HSdll.dll /concordium-consensus/HSdll.dll.a +/concordium-consensus/*.dat +/concordium-consensus/lib concordium-node/deps/static-libs .dir-locals.el hie.yaml @@ -56,6 +58,7 @@ hie.yaml .DS_Store /concordium-node/*.log /stack.yaml.lock +/stack.static.yaml.lock # MacOS distribution /scripts/distribution/macOS-package/tools @@ -67,4 +70,5 @@ xcuserdata/ lcov.info # Test artefacts .stack-work-coverage* -*.blob \ No newline at end of file +*.blob +plt/plt-types/proptest-regressions/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f1583b06fe..d1f784add9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ ## Unreleased changes +## Unreleased changes (Devnet) + +- Add P11 `tokenParameters` authorization and queued `maxLockDuration` chain update support for governing the maximum relative PLT lock duration. + Expired requests are rejected with `LockExpired`; requests beyond the inclusive deadline are rejected with `LockDurationTooLong`. + +- **BREAKING**: Change the storage model for lock recipients to it's permanent variant. +- Added lock metadata as part of lock configurations + +## 11.2.2 (Devnet) + +- Add support for lock configurations with "any" specified for the `recipients` field. +- `tokenTransfer` and `tokenBurn` operations now correctly check the available balance of the token on the account. + +## 11.2.1 (Devnet) + +- Add support for the following "meta update" operations: + - `lockFund`: move protocol-level tokens from an account's available balance into a PLT Lock. + - `lockSend`: move protocol-level tokens from a PLT Lock to a recipient account's available balance. + - `lockReturn`: release protocol-level tokens from a PLT Lock back to the owner's available balance. + - `lockCancel`: cancel a PLT Lock, releasing all funds to their owners. +- Populate the protocol-level token account `module_state` returned by `GetAccountInfo` with available balance and lock details. + +## 11.2.0 (Devnet) + +- Extend gRPC API with `GetLockList` (streaming `LockId`) and `GetLockInfo` (single CBOR-encoded `LockInfo`) v2 endpoints for inspecting protocol-level locks. +- Added support for the new "meta update" transaction, which supports the same operations as any "token update" transaction + and additionally supports the following operations: + - `lockCreate`: create a PLT Lock from a given lock configuration + +# 11.1.0 (Devnet) + +- Support token operations in P11: + - `assignAdminRoles` and `revokeAdminRoles` for managing admin roles for a protocol-level token. + - `updateMetadata` for setting the metadata URL and checksum of a protocol-level token. +- Extend gRPC API with `getTokenAuthorizations` query for listing accounts holding roles for a given protocol-level token. + +# 11.0.0 (DevNet) + +- Protocol level tokens logic has been rewritten in Rust (no behavioral change from P9/P10) + +## 10.0.10 + +- Fix a bug where pending blocks with unknown parents are relayed to peers. + ## 10.0.9 - Upgraded rust version to 1.94 @@ -20,7 +64,7 @@ - Prohibit peers from sending unsolicited PeerList messages - Enhance node performance by limiting inbound queue saturation from peers that send messages aggressively by using backpressure. -- Introduce a background queue for processing messages that don't require the global block state lock. +- Introduce a background queue for processing messages that don't require the global block state lock. # 10.0.5 @@ -31,7 +75,7 @@ - Add recovery from corrupted databases that may be created when protocol updates are executed twice (due to bugs in 8.1.0 - 10.0.1 versions). - + # 10.0.3 - Fix another bug in protocol update state migration that incorrectly migrated PLT state. diff --git a/collector/src/bin/collector.rs b/collector/src/bin/collector.rs index 0f01688977..4844482a70 100644 --- a/collector/src/bin/collector.rs +++ b/collector/src/bin/collector.rs @@ -11,11 +11,10 @@ use tonic::transport::{channel::Channel, ClientTlsConfig}; #[macro_use] extern crate log; -//added more allow here as some generated code invoked warnings #[allow( + unused, clippy::large_enum_variant, clippy::enum_variant_names, - dead_code, clippy::doc_overindented_list_items )] mod grpc { diff --git a/concordium-base b/concordium-base index 92ed21a466..7df016cbb4 160000 --- a/concordium-base +++ b/concordium-base @@ -1 +1 @@ -Subproject commit 92ed21a466d72833b57b2ea3b871f76ee100e123 +Subproject commit 7df016cbb415bcac238174779d18b76a2e10582e diff --git a/concordium-consensus/README.md b/concordium-consensus/README.md index 3fd930e119..8a8ef49b0a 100644 --- a/concordium-consensus/README.md +++ b/concordium-consensus/README.md @@ -25,7 +25,7 @@ This might happen if Rust is installed with the MSVC [ABI](https://en.wikipedia. You can check this by running `rustup show`. If Rust is using the MSVC toolchain you can switch to GNU instead by running ``` -rustup default stable-x86_64-pc-windows-gnu +rustup override set 1.94-x86_64-pc-windows-gnu ``` ### `user specified .o/.so/.DLL could not be loaded (addDLL: pthread or dependencies not loaded. (Win32 error 5)) whilst trying to load: (dynamic) pthread` diff --git a/concordium-consensus/Setup.hs b/concordium-consensus/Setup.hs index 640b57dca3..4338162372 100644 --- a/concordium-consensus/Setup.hs +++ b/concordium-consensus/Setup.hs @@ -4,52 +4,56 @@ 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 the Rust node library workspace relative to this file. +nodeRustLibraryWorkspaceRelative = "../plt" + +postConfHook :: Args -> ConfigFlags -> PackageDescription -> LocalBuildInfo -> IO () +postConfHook 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 PLT scheduler project + nodeRustLibraryWorkspace <- canonicalizePath nodeRustLibraryWorkspaceRelative + withCurrentDirectory nodeRustLibraryWorkspace $ runCmd verbosity $ "rustup show active-toolchain" + withCurrentDirectory nodeRustLibraryWorkspace $ runCmd verbosity $ "cargo build --release --locked -p node-rust-library" 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 " ++ nodeRustLibraryWorkspace ++ "/target/release/node_rust_library.dll " ++ libraryDestination + OSX -> do + runCmd verbosity $ "ln -s -f " ++ nodeRustLibraryWorkspace ++ "/target/release/libnode_rust_library.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ nodeRustLibraryWorkspace ++ "/target/release/libnode_rust_library.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"] - return emptyHookedBuildInfo + runCmd verbosity $ "ln -s -f " ++ nodeRustLibraryWorkspace ++ "/target/release/libnode_rust_library.a " ++ libraryDestination + runCmd verbosity $ "ln -s -f " ++ nodeRustLibraryWorkspace ++ "/target/release/libnode_rust_library.so " ++ libraryDestination + return () -- | 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 PLT scheduler + nodeRustLibraryWorkspace <- canonicalizePath nodeRustLibraryWorkspaceRelative + runCmd verbosity $ "cp -u " ++ nodeRustLibraryWorkspace ++ "/target/release/node_rust_library.dll " ++ bindir installDirs _ -> return () where distPref = fromFlag (copyDistPref flags) @@ -59,6 +63,6 @@ copyDlls _ flags pkgDescr lbi = case buildOS of main = defaultMainWithHooks $ simpleUserHooks - { preConf = makeRust, - postCopy = copyDlls + { postConf = postConfHook, + postCopy = postCopyHook } diff --git a/concordium-consensus/benchmarks/transactions/SchedulerBench/Helpers.hs b/concordium-consensus/benchmarks/transactions/SchedulerBench/Helpers.hs index fe63bbc095..a6a120c5e9 100644 --- a/concordium-consensus/benchmarks/transactions/SchedulerBench/Helpers.hs +++ b/concordium-consensus/benchmarks/transactions/SchedulerBench/Helpers.hs @@ -45,7 +45,7 @@ import Concordium.GlobalState.Types import Concordium.Logger import Concordium.Scheduler import qualified Concordium.Scheduler.DummyData as DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import qualified Concordium.Scheduler.Types as Types import Concordium.TimeMonad import qualified Data.Bifunctor as Bifunctor @@ -60,6 +60,27 @@ getResults = map $ Bifunctor.second Types.tsResult simpleTransferCost :: Types.Energy simpleTransferCost = Cost.baseCost (Types.transactionHeaderSize + 41) 1 + Cost.simpleTransferCost +-- | Call a function for each protocol version, returning a list of results. +-- Notice the return type for the function must be independent of the protocol version. +-- +-- This is used to run a test against every protocol version. +forEveryProtocolVersion :: + (forall pv. (Types.IsProtocolVersion pv) => Types.SProtocolVersion pv -> String -> a) -> + [a] +forEveryProtocolVersion check = + [ check Types.SP1 "P1", + check Types.SP2 "P2", + check Types.SP3 "P3", + check Types.SP4 "P4", + check Types.SP5 "P5", + check Types.SP6 "P6", + check Types.SP7 "P7", + check Types.SP8 "P8", + check Types.SP9 "P9", + check Types.SP10 "P10", + check Types.SP11 "P11" + ] + -- | Monad that implements the necessary constraints to be used for running the scheduler. newtype PersistentBSM pv a = PersistentBSM { _runPersistentBSM :: @@ -98,6 +119,7 @@ deriving instance instance MonadLogger (PersistentBSM pv) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () instance TimeMonad (PersistentBSM pv) where currentTime = return $ read "1970-01-01 13:27:13.257285424 UTC" @@ -117,7 +139,7 @@ createTestBlockStateWithAccounts accounts = do DummyData.dummyIdentityProviders DummyData.dummyArs keys - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @pv) -- save block state and accounts. void $ BS.saveBlockState bs void $ BS.saveGlobalMaps bs diff --git a/concordium-consensus/benchmarks/transactions/TransactionsBench.hs b/concordium-consensus/benchmarks/transactions/TransactionsBench.hs index ae3b5eaf55..ab46d62496 100644 --- a/concordium-consensus/benchmarks/transactions/TransactionsBench.hs +++ b/concordium-consensus/benchmarks/transactions/TransactionsBench.hs @@ -1,6 +1,10 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Timing of various types of transaction @@ -24,16 +28,19 @@ import Concordium.Types.ProtocolLevelTokens.CBOR (TokenUpdateTransaction (TokenU import qualified Concordium.Types.ProtocolLevelTokens.CBOR as CBOR import Concordium.Types.Tokens import Control.DeepSeq +import Control.Monad import Criterion import Criterion.Main +import Data.Bool.Singletons import qualified Data.ByteString as BS -import qualified Data.ByteString.Short as BSS import qualified Data.Map as Map +import Data.Maybe import qualified Data.Sequence as Seq import qualified SchedulerBench.Helpers as Helpers initialBlockState :: - Helpers.PersistentBSM 'Types.P9 (BS.HashedPersistentBlockState 'Types.P9) + (IsProtocolVersion pv) => + Helpers.PersistentBSM pv (BS.HashedPersistentBlockState pv) initialBlockState = Helpers.createTestBlockStateWithAccountsM [ Helpers.makeTestAccountFromSeed 1_000_000_000 0, @@ -43,19 +50,18 @@ initialBlockState = assertApplied :: Bool -> Int -> Helpers.SchedulerResult (TransactionOutcomesVersionFor pv) -> BS.PersistentBlockState pv -> Helpers.PersistentBSM pv () assertApplied assertSuccess txnCount result _state = do let results = Helpers.getResults $ ftAdded (Helpers.srTransactions result) - if length results /= txnCount - then error ("expected " ++ show txnCount ++ " results, was " ++ show (length results)) - else - seq - ( foldl' - ( \_ item -> case snd item of - Exec.TxReject _ | assertSuccess -> error ("failed transaction " ++ show item) - _ -> () - ) - () - results - ) - (return ()) + seq + ( foldl' + ( \_ item -> case snd item of + Exec.TxReject _ | assertSuccess -> error ("failed transaction " ++ show item) + _ -> () + ) + () + results + ) + (return ()) + when (length results /= txnCount) $ + error ("expected " ++ show txnCount ++ " results, was " ++ show (length results)) accountAddress0 :: Types.AccountAddress accountAddress0 = Helpers.accountAddressFromSeed 0 @@ -97,15 +103,15 @@ createPltBlockItem tokenId initializationParameters = ctSeqNumber = 1 } where - toTokenParam = Types.TokenParameter . BSS.toShort . CBOR.tokenInitializationParametersToBytes + toTokenParam = Types.rawCborFromBytes . CBOR.tokenInitializationParametersToBytes createPlt = Types.CreatePLT - { _cpltTokenModule = TokenModuleRef dummyHash, + { _cpltTokenModule = tokenModuleV0Ref, _cpltTokenId = tokenId, _cpltInitializationParameters = toTokenParam initializationParameters, _cpltDecimals = 6 } - dummyHash = Hash.hashShort BSS.empty + tokenModuleV0Ref = TokenModuleRef $ Hash.hash "TokenModuleV0" -- | CCD transfer transaction transferTxn :: SigScheme.KeyPair -> Nonce -> AccountAddress -> AccountAddress -> Amount -> Runner.TransactionJSON @@ -132,9 +138,9 @@ pltTxn keyPair nonce tokenId from operations = } ) where - toTokenParam = Types.TokenParameter . BSS.toShort . CBOR.tokenUpdateTransactionToBytes + toTokenParam = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes -pltTxnFromParam :: SigScheme.KeyPair -> Nonce -> Energy -> TokenId -> AccountAddress -> Types.TokenParameter -> Runner.TransactionJSON +pltTxnFromParam :: SigScheme.KeyPair -> Nonce -> Energy -> TokenId -> AccountAddress -> Types.RawCbor -> Runner.TransactionJSON pltTxnFromParam keyPair nonce cost tokenId from param = Runner.TJSON { metadata = makeDummyHeader from nonce cost, @@ -215,17 +221,36 @@ operationScaleFactor :: Int operationScaleFactor = 1000 -- | Run benchmark on given transactions -benchTransactionsAssertSuccess :: String -> [Runner.TransactionJSON] -> Benchmark -benchTransactionsAssertSuccess label transactions = benchBlockItemsAssertSuccess label (Runner.AccountTx <$> transactions) +benchTransactionsAssertSuccess :: + forall pv. + (IsProtocolVersion pv) => + SProtocolVersion pv -> + String -> + [Runner.TransactionJSON] -> + Benchmark +benchTransactionsAssertSuccess spv label transactions = benchBlockItemsAssertSuccess spv label (Runner.AccountTx <$> transactions) -- | Run benchmark on given block items -benchBlockItemsAssertSuccess :: String -> [Runner.BlockItemDescription] -> Benchmark -benchBlockItemsAssertSuccess label = benchBlockItems label True +benchBlockItemsAssertSuccess :: + forall pv. + (IsProtocolVersion pv) => + SProtocolVersion pv -> + String -> + [Runner.BlockItemDescription] -> + Benchmark +benchBlockItemsAssertSuccess spv label = benchBlockItems spv label True -- | Run benchmark on given block items -benchBlockItems :: String -> Bool -> [Runner.BlockItemDescription] -> Benchmark -benchBlockItems label assertSuccess blockItems = - env (pure initialBlockState) $ \ibs -> +benchBlockItems :: + forall pv. + (IsProtocolVersion pv) => + SProtocolVersion pv -> + String -> + Bool -> + [Runner.BlockItemDescription] -> + Benchmark +benchBlockItems _spv label assertSuccess blockItems = + env (pure $ initialBlockState @pv) $ \ibs -> bench label $ whnfAppIO ( \bis -> do @@ -239,8 +264,11 @@ benchBlockItems label assertSuccess blockItems = blockItems -- | Benchmark `operationScaleFactor` number of CCD transfer transactions -benchTransfer :: Benchmark -benchTransfer = benchTransactionsAssertSuccess "transfer (CCD)" $ transactions operationScaleFactor +benchTransfer :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchTransfer spv = benchTransactionsAssertSuccess spv ("transfer (CCD) transaction x" ++ show operationScaleFactor) $ transactions operationScaleFactor where transactions :: Int -> [Runner.TransactionJSON] transactions txnCount = @@ -251,10 +279,14 @@ benchTransfer = benchTransactionsAssertSuccess "transfer (CCD)" $ transactions o [1 .. txnCount] -- | Benchmark `operationScaleFactor` number of PLT transfer operations in a single transaction -benchPltTransfer :: Benchmark -benchPltTransfer = +benchPltTransfer :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltTransfer spv = benchBlockItemsAssertSuccess - "PLT transfer" + spv + ("PLT transfer operation x" ++ show operationScaleFactor ++ " in a single token update transaction") [ createPltBlockItem plt1 $ tokenInitializationParameters accountAddress0, Runner.AccountTx transaction ] @@ -264,10 +296,14 @@ benchPltTransfer = operations txnCount = replicate txnCount $ transferPltOp accountAddress1 1_000 -- | Benchmark `operationScaleFactor` number of PLT mint operations in a single transaction -benchPltMint :: Benchmark -benchPltMint = +benchPltMint :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltMint spv = benchBlockItemsAssertSuccess - "PLT mint" + spv + ("PLT mint operation x" ++ show operationScaleFactor ++ " in a single token update transaction") [ createPltBlockItem plt1 $ tokenInitializationParameters accountAddress0, Runner.AccountTx transaction ] @@ -277,10 +313,14 @@ benchPltMint = operations txnCount = replicate txnCount $ mintPltOp 1_000 -- | Benchmark `operationScaleFactor` number of PLT burn operations in a single transaction -benchPltBurn :: Benchmark -benchPltBurn = +benchPltBurn :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltBurn spv = benchBlockItemsAssertSuccess - "PLT burn" + spv + ("PLT burn operation x" ++ show operationScaleFactor ++ " in a single token update transaction") [ createPltBlockItem plt1 $ tokenInitializationParameters accountAddress0, Runner.AccountTx transaction ] @@ -290,10 +330,14 @@ benchPltBurn = operations txnCount = replicate txnCount $ burnPltOp 1_000 -- | Benchmark `operationScaleFactor` total number of PLT add and remove from allow list operations in a single transaction -benchPltAddRemoveAllowList :: Benchmark -benchPltAddRemoveAllowList = +benchPltAddRemoveAllowList :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltAddRemoveAllowList spv = benchBlockItemsAssertSuccess - "PLT add/remove allow list" + spv + ("PLT add/remove allow list operation x" ++ show operationScaleFactor ++ " in a single token update transaction") [ createPltBlockItem plt1 (tokenInitializationParameters accountAddress0){CBOR.tipAllowList = Just True}, Runner.AccountTx transaction ] @@ -303,10 +347,14 @@ benchPltAddRemoveAllowList = operations txnCount = take txnCount $ cycle [addAllowListPltOp accountAddress1, removeAllowListPltOp accountAddress1] -- | Benchmark `operationScaleFactor` total number of PLT add and remove from deny list operations in a single transaction -benchPltAddRemoveDenyList :: Benchmark -benchPltAddRemoveDenyList = +benchPltAddRemoveDenyList :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltAddRemoveDenyList spv = benchBlockItemsAssertSuccess - "PLT add/remove deny list" + spv + ("PLT add/remove deny list operation x" ++ show operationScaleFactor ++ " in a single token update transaction") [ createPltBlockItem plt1 $ (tokenInitializationParameters accountAddress0){CBOR.tipDenyList = Just True}, Runner.AccountTx transaction ] @@ -316,10 +364,14 @@ benchPltAddRemoveDenyList = operations txnCount = take txnCount $ cycle [addDenyListPltOp accountAddress1, removeDenyListPltOp accountAddress1] -- | Benchmark `operationScaleFactor` number of PLT transactions with no operations -benchPltNoOperations :: Benchmark -benchPltNoOperations = +benchPltNoOperations :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltNoOperations spv = benchBlockItemsAssertSuccess - "PLT no operations" + spv + ("PLT token update transaction with no operations x" ++ show operationScaleFactor) ( createPltBlockItem plt1 (tokenInitializationParameters accountAddress0) : (Runner.AccountTx <$> transactions operationScaleFactor) ) @@ -334,10 +386,14 @@ benchPltNoOperations = -- | Benchmark `operationScaleFactor` number of PLT transaction each with a single PLT transfer operation. Benchmark should be equal -- to sum of PLT transaction with no operations plus PLT transfer operation -benchPltTxnAndTransfer :: Benchmark -benchPltTxnAndTransfer = +benchPltTxnAndTransfer :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltTxnAndTransfer spv = benchBlockItemsAssertSuccess - "PLT txn + PLT transfer" + spv + ("PLT token update transaction with single PLT transfer operation x" ++ show operationScaleFactor) ( createPltBlockItem plt1 (tokenInitializationParameters accountAddress0) : (Runner.AccountTx <$> transactions operationScaleFactor) ) @@ -353,10 +409,14 @@ benchPltTxnAndTransfer = operation = transferPltOp accountAddress1 1_000 -- | Benchmark `operationScaleFactor` number of PLT transactions with invalid CBOR -benchPltTxnCborDecodeError :: Benchmark -benchPltTxnCborDecodeError = +benchPltTxnCborDecodeError :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchPltTxnCborDecodeError spv = benchBlockItems - "PLT cbor decode error" + spv + ("PLT cbor decode error x" ++ show operationScaleFactor) False ( createPltBlockItem plt1 (tokenInitializationParameters accountAddress0) : (Runner.AccountTx <$> transactions operationScaleFactor) @@ -369,7 +429,7 @@ benchPltTxnCborDecodeError = pltTxnFromParam keyPair0 (fromIntegral nonce) (Helpers.simpleTransferCost * 5) plt1 accountAddress0 invalidParam ) [1 .. txnCount] - invalidParam = Types.TokenParameter $ BSS.toShort $ BS.snoc param 0 + invalidParam = Types.rawCborFromBytes $ BS.snoc param 0 param = CBOR.tokenUpdateTransactionToBytes $ TokenUpdateTransaction @@ -380,20 +440,34 @@ benchPltTxnCborDecodeError = operation = transferPltOp accountAddress1 1_000 -- | Benchmark running no transactions (test framework overhead) -benchNoTxns :: Benchmark -benchNoTxns = benchBlockItemsAssertSuccess "no txns (overhead)" [] +benchNoTxns :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> Benchmark +benchNoTxns spv = benchBlockItemsAssertSuccess spv ("no txns (test overhead) x" ++ show operationScaleFactor) [] main :: IO () main = - defaultMain - [ benchTransfer, - benchPltTransfer, - benchNoTxns, - benchPltMint, - benchPltBurn, - benchPltNoOperations, - benchPltAddRemoveAllowList, - benchPltAddRemoveDenyList, - benchPltTxnAndTransfer, - benchPltTxnCborDecodeError - ] + defaultMain $ + catMaybes $ + Helpers.forEveryProtocolVersion benches + where + benches :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> String -> Maybe Benchmark + benches spv pvString = + case sSupportsPLT (sAccountVersionFor spv) of + STrue -> + Just $ + bgroup + pvString + [ benchTransfer spv, + benchPltTransfer spv, + benchNoTxns spv, + benchPltMint spv, + benchPltBurn spv, + benchPltNoOperations spv, + benchPltAddRemoveAllowList spv, + benchPltAddRemoveDenyList spv, + benchPltTxnAndTransfer spv, + benchPltTxnCborDecodeError spv + ] + SFalse -> Nothing diff --git a/concordium-consensus/lib.def b/concordium-consensus/lib.def index bd2c206fdf..36de8fce2e 100644 --- a/concordium-consensus/lib.def +++ b/concordium-consensus/lib.def @@ -24,6 +24,9 @@ EXPORTS getAccountInfoV2 getTokenInfoV2 + getTokenAuthorizationsV2 + getLockListV2 + getLockInfoV2 getAccountListV2 getTokenListV2 getModuleListV2 diff --git a/concordium-consensus/package.yaml b/concordium-consensus/package.yaml index 160d91acb3..19b2b61a42 100644 --- a/concordium-consensus/package.yaml +++ b/concordium-consensus/package.yaml @@ -88,7 +88,8 @@ library: - -O2 - -fno-ignore-asserts - extra-libraries: concordium_smart_contract_engine + extra-libraries: + - node_rust_library when: - condition: "!(os(windows)) && !(flag(dynamic))" diff --git a/concordium-consensus/src-lib/Concordium/External/DryRun.hs b/concordium-consensus/src-lib/Concordium/External/DryRun.hs index cad90e55c2..066284d048 100644 --- a/concordium-consensus/src-lib/Concordium/External/DryRun.hs +++ b/concordium-consensus/src-lib/Concordium/External/DryRun.hs @@ -61,7 +61,6 @@ import Concordium.MultiVersion import Concordium.Queries import qualified Concordium.Scheduler as Scheduler import qualified Concordium.Scheduler.Environment as Scheduler -import qualified Concordium.Scheduler.EnvironmentImplementation as Scheduler import qualified Concordium.Scheduler.InvokeContract as InvokeContract import qualified Concordium.Skov as SkovV0 import qualified Concordium.TransactionVerification as TVer diff --git a/concordium-consensus/src-lib/Concordium/External/GRPC2.hs b/concordium-consensus/src-lib/Concordium/External/GRPC2.hs index 5232c6ef35..bbcb714883 100644 --- a/concordium-consensus/src-lib/Concordium/External/GRPC2.hs +++ b/concordium-consensus/src-lib/Concordium/External/GRPC2.hs @@ -42,7 +42,11 @@ import Concordium.Crypto.SHA256 (Hash (Hash)) import Concordium.External.Helpers import Concordium.GlobalState.Parameters (CryptographicParameters) import Concordium.ID.Parameters (withGlobalContext) -import Concordium.Scheduler.ProtocolLevelTokens.Queries (QueryTokenInfoError (..)) +import Concordium.Scheduler.ProtocolLevelTokens.Queries ( + QueryLockError (..), + QueryTokenModuleError (..), + SerializedLockId, + ) import qualified Concordium.Types.InvokeContract as InvokeContract import qualified Concordium.Wasm as Wasm @@ -158,18 +162,106 @@ getTokenInfoV2 cptr blockType blockHashPtr tokenIdPtr tokenIdLen outHash outVec Right tokenId -> do res <- runMVR (Q.getTokenInfo bhi tokenId) mvr case res of - Q.BQRBlock _ (Left QTIEUnknownToken) -> do + Q.BQRBlock _ (Left QTMEUnknownToken) -> do copyHashTo outHash res return $ queryResultCode QRNotFound - Q.BQRBlock _ (Left e@QTIEInternal{}) -> do + Q.BQRBlock _ (Left e@QTMEInternal{}) -> do mvLog mvr Logger.External Logger.LLError $ "Internal error processing GetTokenInfo: " ++ show e return $ queryResultCode QRInternalError + Q.BQRBlock _ (Left QTMEUnavailable) -> do + copyHashTo outHash res + return $ queryResultCode QRUnavailable Q.BQRBlock _ (Right r) -> returnMessageWithBlock (copier outVec) outHash (res $> r) Q.BQRNoBlock -> return $ queryResultCode QRNotFound +getTokenAuthorizationsV2 :: + StablePtr Ext.ConsensusRunner -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Token ID. + Ptr Word8 -> + -- | Token ID length. + Word8 -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + Ptr ReceiverVec -> + -- | Callback to output data. + FunPtr CopyToVecCallback -> + IO Int64 +getTokenAuthorizationsV2 cptr blockType blockHashPtr tokenIdPtr tokenIdLen outHash outVec copierCbk = do + Ext.ConsensusRunner mvr <- deRefStablePtr cptr + let copier = callCopyToVecCallback copierCbk + bhi <- decodeBlockHashInput blockType blockHashPtr + decodeTokenId tokenIdPtr tokenIdLen >>= \case + Left _ -> return $ queryResultCode QRInvalidArgument + Right tokenId -> do + res <- runMVR (Q.getTokenAuthorizations bhi tokenId) mvr + case res of + Q.BQRBlock _ (Left QTMEUnknownToken) -> do + copyHashTo outHash res + return $ queryResultCode QRNotFound + Q.BQRBlock _ (Left e@QTMEInternal{}) -> do + mvLog mvr Logger.External Logger.LLError $ + "Internal error processing GetTokenAuthorizations: " ++ show e + return $ queryResultCode QRInternalError + Q.BQRBlock _ (Left QTMEUnavailable) -> do + copyHashTo outHash res + return $ queryResultCode QRUnavailable + Q.BQRBlock _ (Right r) -> + returnMessageWithBlock (copier outVec) outHash (res $> r) + Q.BQRNoBlock -> + return $ queryResultCode QRNotFound + +-- | Foreign-exported FFI entry point for the streaming `GetLockList` gRPC v2 endpoint. +-- Streams the list of all PLT lock ids that exist at the end of the resolved block. +getLockListV2 :: + StablePtr Ext.ConsensusRunner -> + Ptr SenderChannel -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + FunPtr (Ptr SenderChannel -> Ptr Word8 -> Int64 -> IO Int32) -> + IO Int64 +getLockListV2 = blockStreamHelper Q.getLockList + +-- | Foreign-exported FFI entry point for the unary `GetLockInfo` gRPC v2 endpoint. +getLockInfoV2 :: + StablePtr Ext.ConsensusRunner -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Lock ID (24 bytes: three big-endian Word64 fields). + Ptr SerializedLockId -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + Ptr ReceiverVec -> + -- | Callback to output data. + FunPtr CopyToVecCallback -> + IO Int64 +getLockInfoV2 cptr blockType blockHashPtr lockIdPtr outHash outVec copierCbk = do + Ext.ConsensusRunner mvr <- deRefStablePtr cptr + let copier = callCopyToVecCallback copierCbk + bhi <- decodeBlockHashInput blockType blockHashPtr + lockId <- peek (castPtr lockIdPtr) + res <- runMVR (Q.getLockInfo bhi lockId) mvr + case res of + Q.BQRBlock _ (Left QLEUnknownLock) -> do + copyHashTo outHash res + return $ queryResultCode QRNotFound + Q.BQRBlock _ (Right r) -> + returnMessageWithBlock (copier outVec) outHash (res $> r) + Q.BQRNoBlock -> + return $ queryResultCode QRNotFound + -- | Optionally copy a block hash (32 bytes) to a pointer. -- Used to provide back the block hash used in a given query, via the FFI. copyHashTo :: Ptr Word8 -> Q.BHIQueryResponse a -> IO () @@ -1301,6 +1393,53 @@ foreign export ccall FunPtr CopyToVecCallback -> IO Int64 +foreign export ccall + getTokenAuthorizationsV2 :: + StablePtr Ext.ConsensusRunner -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Token ID. + Ptr Word8 -> + -- | Token ID length. + Word8 -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + Ptr ReceiverVec -> + -- | Callback to output data. + FunPtr CopyToVecCallback -> + IO Int64 + +foreign export ccall + getLockInfoV2 :: + StablePtr Ext.ConsensusRunner -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Lock ID (24 bytes: three big-endian Word64 fields). + Ptr SerializedLockId -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + Ptr ReceiverVec -> + -- | Callback to output data. + FunPtr CopyToVecCallback -> + IO Int64 + +foreign export ccall + getLockListV2 :: + StablePtr Ext.ConsensusRunner -> + Ptr SenderChannel -> + -- | Block type. + Word8 -> + -- | Block hash. + Ptr Word8 -> + -- | Out pointer for writing the block hash that was used. + Ptr Word8 -> + FunPtr (Ptr SenderChannel -> Ptr Word8 -> Int64 -> IO Int32) -> + IO Int64 + foreign export ccall getAccountListV2 :: StablePtr Ext.ConsensusRunner -> diff --git a/concordium-consensus/src/Concordium/GlobalState/BakerInfo.hs b/concordium-consensus/src/Concordium/GlobalState/BakerInfo.hs index 40eed9e2f1..7ea2c4d0ee 100644 --- a/concordium-consensus/src/Concordium/GlobalState/BakerInfo.hs +++ b/concordium-consensus/src/Concordium/GlobalState/BakerInfo.hs @@ -378,6 +378,7 @@ genesisBakerInfoEx spv cp GenesisBaker{..} = case spv of SP8 -> binfoV1 SP9 -> binfoV1 SP10 -> binfoV1 + SP11 -> binfoV1 where bkrInfo = BakerInfo diff --git a/concordium-consensus/src/Concordium/GlobalState/Block.hs b/concordium-consensus/src/Concordium/GlobalState/Block.hs index 3ef3f70c47..0d892be584 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Block.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Block.hs @@ -145,6 +145,7 @@ blockVersion SP7 = 3 blockVersion SP8 = 3 blockVersion SP9 = 3 blockVersion SP10 = 3 +blockVersion SP11 = 3 {-# INLINE blockVersion #-} -- | Type class that supports serialization of a block. diff --git a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs index f64c31df5f..c2b4505419 100644 --- a/concordium-consensus/src/Concordium/GlobalState/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/BlockState.hs @@ -3,6 +3,7 @@ {-# LANGUAGE DerivingVia #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeApplications #-} @@ -92,13 +93,14 @@ import Concordium.GlobalState.ContractStateFFIHelpers (LoadCallback) import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import Concordium.GlobalState.CooldownQueue (Cooldowns) import qualified Concordium.GlobalState.Persistent.Account.ProtocolLevelTokens as GSAccount +import qualified Concordium.GlobalState.Persistent.BlockState.ExternalChainParameters as ECP import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens ( PLTConfiguration, - ProtocolLevelTokensHash (..), TokenIndex, TokenStateKey, TokenStateValue, ) +import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState as RustBS import Concordium.GlobalState.Persistent.LMDB (FixedSizeSerialization) import Concordium.GlobalState.TransactionTable (TransactionTable) import Concordium.ID.Parameters (GlobalContext) @@ -137,7 +139,7 @@ data BlockStateHashInputs (pv :: ProtocolVersion) = BlockStateHashInputs bshBlockRewardDetails :: BlockRewardDetailsHash pv, -- | The protocol level tokens hash is present from protocol version 9. bshProtocolLevelTokens :: - Conditionally (SupportsPLT (AccountVersionFor pv)) ProtocolLevelTokensHash + Conditionally (PltStatePresent (PltStateVersionFor pv)) ProtocolLevelTokensHash } deriving (Show) @@ -522,33 +524,37 @@ class (MonadProtocolVersion m, Monad m, TokenStateOperations ts m) => PLTQuery b -- | 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. - getPLTList :: bs -> m [TokenId] + getPLTList :: (PVSupportsHaskellManagedPLT (MPV m)) => bs -> m [TokenId] -- | Get the 'TokenIndex' associated with a 'TokenId' (if it exists). - getTokenIndex :: (PVSupportsPLT (MPV m)) => bs -> TokenId -> m (Maybe TokenIndex) + getTokenIndex :: (PVSupportsHaskellManagedPLT (MPV m)) => bs -> TokenId -> m (Maybe TokenIndex) -- | 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 'bsoSetTokenState'. -- -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. - getMutableTokenState :: (PVSupportsPLT (MPV m)) => bs -> TokenIndex -> m ts + getMutableTokenState :: (PVSupportsHaskellManagedPLT (MPV m)) => bs -> TokenIndex -> m ts -- | Get the configuration of a protocol-level token. -- -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. - getTokenConfiguration :: (PVSupportsPLT (MPV m)) => bs -> TokenIndex -> m PLTConfiguration + getTokenConfiguration :: (PVSupportsHaskellManagedPLT (MPV m)) => bs -> TokenIndex -> m PLTConfiguration -- | Get the circulating supply of a protocol-level token. -- -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. - getTokenCirculatingSupply :: (PVSupportsPLT (MPV m)) => bs -> TokenIndex -> m TokenRawAmount + getTokenCirculatingSupply :: (PVSupportsHaskellManagedPLT (MPV m)) => bs -> TokenIndex -> m TokenRawAmount -- | The block query methods can query block state. They are needed by -- consensus itself to compute stake, get a list of and information about -- bakers, finalization committee, etc. class - (ContractStateOperations m, AccountOperations m, ModuleQuery m, PLTQuery (BlockState m) (MutableTokenState m) m) => + ( ContractStateOperations m, + AccountOperations m, + ModuleQuery m, + PLTQuery (BlockState m) (MutableTokenState m) m + ) => BlockStateQuery m where -- | Get the module source from the module table as deployed to the chain. @@ -758,6 +764,41 @@ class -- | Get the index of accounts in pre-pre-cooldown. getPrePreCooldownAccounts :: BlockState m -> m [AccountIndex] + -- | Get the foreign pointer to the Rust managed PLT state. + -- + -- This is a Low-level interface needed for foreign function interface access. + getRustPLTBlockState :: + (PVSupportsRustManagedPLT (MPV m)) => + BlockState m -> m (RustBS.ForeignPLTBlockStatePtr (MPV m)) + + -- | Lifts 'MonadBlobStore' action into the 'BlockStateOperations' monad. + -- + -- This is a Low-level interface needed for foreign function interface access. + liftBlobStore :: + ( forall m'. + ( MonadBlobStore m', + MPV m ~ MPV m' + ) => + m' a + ) -> + m a + + -- | Allows construction of an IO action in a context where 'BlockStateQuery' actions + -- can be unlifted into the IO monad. The resulting IO action is then lifted + -- in to the 'BlockStateQuery' monad and returned. + -- + -- This is a Low-level interface needed for foreign function interface access. + withUnliftBSQ :: + ( forall m'. + ( BlockStateQuery m', + MPV m ~ MPV m', + BlockState m ~ BlockState m', + Account m ~ Account m' + ) => + (forall a. m' a -> IO a) -> IO b + ) -> + m b + -- | Distribution of newly-minted GTU. data MintAmounts = MintAmounts { -- | Minted amount allocated to the BakingRewardAccount @@ -843,7 +884,12 @@ type ActiveBakerInfo m = ActiveBakerInfo' (BakerInfoRef m) -- | Block state update operations parametrized by a monad. The operations which -- mutate the state all also return an 'UpdatableBlockState' handle. This is to -- support different implementations, from pure ones to stateful ones. -class (BlockStateQuery m, PLTQuery (UpdatableBlockState m) (MutableTokenState m) m) => BlockStateOperations m where +class + ( BlockStateQuery m, + PLTQuery (UpdatableBlockState m) (MutableTokenState m) m + ) => + BlockStateOperations m + where -- | Get the module from the module table of the state instance. bsoGetModule :: UpdatableBlockState m -> ModuleRef -> m (Maybe (GSWasm.ModuleInterface (InstrumentedModuleRef m))) @@ -1559,7 +1605,7 @@ class (BlockStateQuery m, PLTQuery (UpdatableBlockState m) (MutableTokenState m) -- To ensure this is future-proof, the mutable state should not be used after this call. -- -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. - bsoSetTokenState :: (PVSupportsPLT (MPV m)) => UpdatableBlockState m -> TokenIndex -> MutableTokenState m -> m (UpdatableBlockState m) + bsoSetTokenState :: (PVSupportsHaskellManagedPLT (MPV m)) => UpdatableBlockState m -> TokenIndex -> MutableTokenState m -> m (UpdatableBlockState m) -- | Overwrite the election difficulty, removing any queued election difficulty updates. -- This is intended to be used for protocol updates that affect the election difficulty in @@ -1651,7 +1697,7 @@ class (BlockStateQuery m, PLTQuery (UpdatableBlockState m) (MutableTokenState m) -- -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. bsoSetTokenCirculatingSupply :: - (PVSupportsPLT (MPV m)) => + (PVSupportsHaskellManagedPLT (MPV m)) => -- | The current block state. UpdatableBlockState m -> -- | The token index to update. @@ -1669,7 +1715,7 @@ class (BlockStateQuery m, PLTQuery (UpdatableBlockState m) (MutableTokenState m) -- @Nothing@. The 'PLTConfiguration' MUST be valid, and in particular the -- '_pltGovernanceAccountIndex' MUST reference a valid account. bsoCreateToken :: - (PVSupportsPLT (MPV m)) => + (PVSupportsHaskellManagedPLT (MPV m)) => -- | The current block state @s@. UpdatableBlockState m -> -- | The configuration for the token @cfg@. @@ -1719,6 +1765,43 @@ class (BlockStateQuery m, PLTQuery (UpdatableBlockState m) (MutableTokenState m) -- | Roll back to the state at the snapshot. This should be used with caution. bsoRollback :: UpdatableBlockState m -> StateSnapshot m -> m (UpdatableBlockState m) + -- | Get the foreign pointer to the Rust managed PLT state. + -- + -- This is a Low-level interface needed for foreign function interface access. + bsoGetRustPLTBlockState :: + (PVSupportsRustManagedPLT (MPV m)) => + UpdatableBlockState m -> m (RustBS.ForeignPLTBlockStatePtr (MPV m)) + + -- | Get the node-owned Rust external chain-parameters pointer. + -- + -- This is a low-level interface needed for foreign function interface access. + bsoGetExternalChainParameters :: + (PVSupportsRustManagedECP (MPV m)) => + UpdatableBlockState m -> m (ECP.ForeignExternalChainParametersPtr (MPV m)) + + -- | Set the foreign pointer to the Rust managed PLT state. + -- + -- This is a Low-level interface needed for foreign function interface access. + bsoSetRustPLTBlockState :: + (PVSupportsRustManagedPLT (MPV m)) => + UpdatableBlockState m -> (RustBS.ForeignPLTBlockStatePtr (MPV m)) -> m (UpdatableBlockState m) + + -- | Allows construction of an IO action in a context where 'BlockStateOperations' actions + -- can be unlifted into the IO monad. The resulting IO action is then lifted + -- in to the 'BlockStateOperations' monad and returned. + -- + -- This is a Low-level interface needed for foreign function interface access. + withUnliftBSO :: + ( forall m'. + ( BlockStateOperations m', + MPV m ~ MPV m', + UpdatableBlockState m ~ UpdatableBlockState m', + Account m ~ Account m' + ) => + (forall a. m' a -> IO a) -> IO b + ) -> + m b + -- | Block state storage operations class (BlockStateOperations m, FixedSizeSerialization (BlockStateRef m)) => BlockStateStorage m where -- | Derive a mutable state instance from a block state instance. The mutable @@ -1901,6 +1984,9 @@ instance (Monad (t m), MonadTrans t, BlockStateQuery m) => BlockStateQuery (MGST getCooldownAccounts = lift . getCooldownAccounts getPreCooldownAccounts = lift . getPreCooldownAccounts getPrePreCooldownAccounts = lift . getPrePreCooldownAccounts + getRustPLTBlockState bs = lift $ getRustPLTBlockState bs + liftBlobStore m = lift $ liftBlobStore m + withUnliftBSQ query = lift $ withUnliftBSQ query {-# INLINE getModule #-} {-# INLINE getAccount #-} {-# INLINE accountExists #-} @@ -1940,6 +2026,9 @@ instance (Monad (t m), MonadTrans t, BlockStateQuery m) => BlockStateQuery (MGST {-# INLINE getCooldownAccounts #-} {-# INLINE getPreCooldownAccounts #-} {-# INLINE getPrePreCooldownAccounts #-} + {-# INLINE getRustPLTBlockState #-} + {-# INLINE liftBlobStore #-} + {-# INLINE withUnliftBSQ #-} instance (Monad (t m), MonadTrans t, AccountOperations m) => AccountOperations (MGSTrans t m) where getAccountCanonicalAddress = lift . getAccountCanonicalAddress @@ -2079,6 +2168,10 @@ instance (Monad (t m), MonadTrans t, BlockStateOperations m) => BlockStateOperat bsoCreateToken s = lift . bsoCreateToken s bsoUpdateTokenAccountBalance s tokIx accIx = lift . bsoUpdateTokenAccountBalance s tokIx accIx bsoTouchTokenAccount s tokIx = lift . bsoTouchTokenAccount s tokIx + bsoGetRustPLTBlockState pbs = lift $ bsoGetRustPLTBlockState pbs + bsoGetExternalChainParameters pbs = lift $ bsoGetExternalChainParameters pbs + bsoSetRustPLTBlockState pbs pltState = lift $ bsoSetRustPLTBlockState pbs pltState + withUnliftBSO operation = lift $ withUnliftBSO operation type StateSnapshot (MGSTrans t m) = StateSnapshot m bsoSnapshotState = lift . bsoSnapshotState bsoRollback s = lift . bsoRollback s @@ -2143,6 +2236,10 @@ instance (Monad (t m), MonadTrans t, BlockStateOperations m) => BlockStateOperat {-# INLINE bsoTouchTokenAccount #-} {-# INLINE bsoSetTokenState #-} {-# INLINE bsoSuspendValidators #-} + {-# INLINE bsoGetRustPLTBlockState #-} + {-# INLINE bsoGetExternalChainParameters #-} + {-# INLINE bsoSetRustPLTBlockState #-} + {-# INLINE withUnliftBSO #-} {-# INLINE bsoSnapshotState #-} {-# INLINE bsoRollback #-} diff --git a/concordium-consensus/src/Concordium/GlobalState/DummyData.hs b/concordium-consensus/src/Concordium/GlobalState/DummyData.hs index 1d1ed5b536..40096944b9 100644 --- a/concordium-consensus/src/Concordium/GlobalState/DummyData.hs +++ b/concordium-consensus/src/Concordium/GlobalState/DummyData.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -95,7 +96,8 @@ dummyAuthorizations = asAddIdentityProvider = theOnly, asCooldownParameters = conditionally (sSupportsCooldownParametersAccessStructure (sing @auv)) theOnly, asTimeParameters = conditionally (sSupportsTimeParameters (sing @auv)) theOnly, - asCreatePLT = conditionally (sSupportsCreatePLT (sing @auv)) theOnly + asCreatePLT = conditionally (sSupportsCreatePLT (sing @auv)) theOnly, + asTokenParameters = conditionally (sSupportsTokenParameters (sing @auv)) theOnly } where theOnly = AccessStructure (Set.singleton 0) 1 @@ -373,8 +375,21 @@ dummyValidatorScoreParameters = _vspMaxMissedRounds = 1 } -dummyChainParameters :: forall cpv. (IsChainParametersVersion cpv) => ChainParameters' cpv -dummyChainParameters = case chainParametersVersion @cpv of +-- | Dummy chain parameters for the given protocol version. +-- +-- For P11 this includes an initial max lock duration, which is required by the +-- node-owned external chain-parameters component. +dummyChainParameters :: forall pv. (IsProtocolVersion pv) => ChainParameters pv +dummyChainParameters = case protocolVersion @pv of + SP11 -> (dummyChainParameters' @(ChainParametersVersionFor pv)){_cpMaxLockDuration = SomeParam (Just (Duration 42))} + _ -> dummyChainParameters' @(ChainParametersVersionFor pv) + +-- | Dummy chain parameters for the given chain-parameters version. +-- +-- This helper is intentionally indexed by chain-parameters version only. For +-- protocol-aware genesis/test data, prefer 'dummyChainParameters'. +dummyChainParameters' :: forall cpv. (IsChainParametersVersion cpv) => ChainParameters' cpv +dummyChainParameters' = case chainParametersVersion @cpv of SChainParametersV0 -> ChainParameters { _cpConsensusParameters = ConsensusParametersV0 $ makeElectionDifficulty 50000, @@ -392,7 +407,8 @@ dummyChainParameters = case chainParametersVersion @cpv of { _ppBakerStakeThreshold = 300000000000 }, _cpFinalizationCommitteeParameters = NoParam, - _cpValidatorScoreParameters = NoParam + _cpValidatorScoreParameters = NoParam, + _cpMaxLockDuration = NoParam } SChainParametersV1 -> ChainParameters @@ -431,7 +447,8 @@ dummyChainParameters = case chainParametersVersion @cpv of } }, _cpFinalizationCommitteeParameters = NoParam, - _cpValidatorScoreParameters = NoParam + _cpValidatorScoreParameters = NoParam, + _cpMaxLockDuration = NoParam } SChainParametersV2 -> ChainParameters @@ -470,7 +487,8 @@ dummyChainParameters = case chainParametersVersion @cpv of } }, _cpFinalizationCommitteeParameters = SomeParam dummyFinalizationCommitteeParameters, - _cpValidatorScoreParameters = NoParam + _cpValidatorScoreParameters = NoParam, + _cpMaxLockDuration = NoParam } SChainParametersV3 -> ChainParameters @@ -509,7 +527,8 @@ dummyChainParameters = case chainParametersVersion @cpv of } }, _cpFinalizationCommitteeParameters = SomeParam dummyFinalizationCommitteeParameters, - _cpValidatorScoreParameters = SomeParam dummyValidatorScoreParameters + _cpValidatorScoreParameters = SomeParam dummyValidatorScoreParameters, + _cpMaxLockDuration = SomeParam Nothing } where fullRange = InclusiveRange (makeAmountFraction 0) (makeAmountFraction 100000) diff --git a/concordium-consensus/src/Concordium/GlobalState/Parameters.hs b/concordium-consensus/src/Concordium/GlobalState/Parameters.hs index e55164459a..40265169be 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Parameters.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Parameters.hs @@ -189,6 +189,8 @@ data UpdateValue (cpv :: ChainParametersVersion) (auv :: AuthorizationsVersion) UVFinalizationCommitteeParameters :: (IsSupported 'PTFinalizationCommitteeParameters cpv ~ 'True) => !FinalizationCommitteeParameters -> UpdateValue cpv auv -- | Updates to the validator score parameters for chain parameters version 3. UVValidatorScoreParameters :: (IsSupported 'PTValidatorScoreParameters cpv ~ 'True) => !ValidatorScoreParameters -> UpdateValue cpv auv + -- | Updates to the maximum relative duration for protocol-level token locks. + UVMaxLockDuration :: (SupportsTokenParameters auv ~ 'True) => !Duration -> UpdateValue cpv auv deriving instance Eq (UpdateValue cpv auv) deriving instance Show (UpdateValue cpv auv) diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account.hs index cabac3c92d..5b2f526c7a 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account.hs @@ -15,6 +15,9 @@ import Control.Arrow import Control.Monad import qualified Data.Map.Strict as Map +import qualified Concordium.Crypto.SHA256 as Hash +import Concordium.Genesis.Data +import Concordium.GlobalState.Persistent.Migration import Concordium.ID.Parameters import Concordium.ID.Types import Concordium.Types @@ -25,8 +28,6 @@ import Concordium.Types.HashableTo import Concordium.Types.Parameters import Concordium.Types.Tokens (TokenRawAmount) -import qualified Concordium.Crypto.SHA256 as Hash -import Concordium.Genesis.Data import Concordium.GlobalState.Account import Concordium.GlobalState.BakerInfo import qualified Concordium.GlobalState.Basic.BlockState.Account as Transient @@ -851,44 +852,40 @@ migratePersistentAccount :: StateMigrationParameters oldpv pv -> PersistentAccount (AccountVersionFor oldpv) -> t m (PersistentAccount (AccountVersionFor pv)) -migratePersistentAccount m@StateMigrationParametersTrivial (PAV0 acc) = PAV0 <$> V0.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersTrivial (PAV1 acc) = PAV1 <$> V0.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersTrivial (PAV2 acc) = PAV2 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersTrivial (PAV3 acc) = PAV3 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersTrivial (PAV4 acc) = PAV4 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersTrivial (PAV5 acc) = PAV5 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP1P2 (PAV0 acc) = PAV0 <$> V0.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP2P3 (PAV0 acc) = PAV0 <$> V0.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP3ToP4{} (PAV0 acc) = PAV1 <$> V0.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP4ToP5{} (PAV1 acc) = PAV2 <$> V1.migratePersistentAccountFromV0 m acc -migratePersistentAccount m@StateMigrationParametersP5ToP6{} (PAV2 acc) = PAV2 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP6ToP7{} (PAV2 acc) = PAV3 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP7ToP8{} (PAV3 acc) = PAV4 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP8ToP9{} (PAV4 acc) = PAV5 <$> V1.migratePersistentAccount m acc -migratePersistentAccount m@StateMigrationParametersP9ToP10{} (PAV5 acc) = PAV5 <$> V1.migratePersistentAccount m acc +migratePersistentAccount m = case accountTypeMigrationFor m of + AccountMigrationTrivial -> \case + PAV0 acc -> PAV0 <$> V0.migratePersistentAccount m acc + PAV1 acc -> PAV1 <$> V0.migratePersistentAccount m acc + PAV2 acc -> PAV2 <$> V1.migrateV2ToV2 acc + PAV3 acc -> PAV3 <$> V1.migrateV3ToV3 acc + PAV4 acc -> PAV4 <$> V1.migrateV4ToV4 acc + PAV5 acc -> PAV5 <$> V1.migrateV5ToV5 acc + AccountMigrationV0ToV1 -> \(PAV0 acc) -> PAV1 <$> V0.migratePersistentAccount m acc + AccountMigrationV1ToV2 -> \(PAV1 acc) -> PAV2 <$> V1.migratePersistentAccountFromV0 m acc + AccountMigrationV2ToV3 -> \(PAV2 acc) -> PAV3 <$> V1.migrateV2ToV3 acc + AccountMigrationV3ToV4 -> \(PAV3 acc) -> PAV4 <$> V1.migrateV3ToV4 acc + AccountMigrationV4ToV5 -> \(PAV4 acc) -> PAV5 <$> V1.migrateV4ToV5 acc -- | Migrate a 'PersistentBakerInfoRef' between protocol versions according to a state migration. migratePersistentBakerInfoRef :: forall oldpv pv t m. - (IsProtocolVersion pv, SupportMigration m t) => + (IsProtocolVersion oldpv, IsProtocolVersion pv, SupportMigration m t) => StateMigrationParameters oldpv pv -> PersistentBakerInfoRef (AccountVersionFor oldpv) -> t m (PersistentBakerInfoRef (AccountVersionFor pv)) -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV0 bir) = PBIRV0 <$> V0.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV1 bir) = PBIRV1 <$> V0.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV2 bir) = PBIRV2 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV3 bir) = PBIRV3 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV4 bir) = PBIRV4 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersTrivial (PBIRV5 bir) = PBIRV5 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP1P2 (PBIRV0 bir) = PBIRV0 <$> V0.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP2P3 (PBIRV0 bir) = PBIRV0 <$> V0.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP3ToP4{} (PBIRV0 bir) = PBIRV1 <$> V0.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP4ToP5{} (PBIRV1 bir) = PBIRV2 <$> V1.migratePersistentBakerInfoExFromV0 m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP5ToP6{} (PBIRV2 bir) = PBIRV2 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP6ToP7{} (PBIRV2 bir) = PBIRV3 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP7ToP8{} (PBIRV3 bir) = PBIRV4 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP8ToP9{} (PBIRV4 bir) = PBIRV5 <$> V1.migratePersistentBakerInfoEx m bir -migratePersistentBakerInfoRef m@StateMigrationParametersP9ToP10{} (PBIRV5 bir) = PBIRV5 <$> V1.migratePersistentBakerInfoEx m bir +migratePersistentBakerInfoRef m = case accountTypeMigrationFor m of + AccountMigrationTrivial -> \case + PBIRV0 bir -> PBIRV0 <$> V0.migratePersistentBakerInfoEx m bir + PBIRV1 bir -> PBIRV1 <$> V0.migratePersistentBakerInfoEx m bir + PBIRV2 bir -> PBIRV2 <$> V1.migratePersistentBakerInfoEx m bir + PBIRV3 bir -> PBIRV3 <$> V1.migratePersistentBakerInfoEx m bir + PBIRV4 bir -> PBIRV4 <$> V1.migratePersistentBakerInfoEx m bir + PBIRV5 bir -> PBIRV5 <$> V1.migratePersistentBakerInfoEx m bir + AccountMigrationV0ToV1 -> \(PBIRV0 bir) -> PBIRV1 <$> V0.migratePersistentBakerInfoEx m bir + AccountMigrationV1ToV2 -> \(PBIRV1 bir) -> PBIRV2 <$> V1.migratePersistentBakerInfoExFromV0 m bir + AccountMigrationV2ToV3 -> \(PBIRV2 bir) -> PBIRV3 <$> V1.migratePersistentBakerInfoEx m bir + AccountMigrationV3ToV4 -> \(PBIRV3 bir) -> PBIRV4 <$> V1.migratePersistentBakerInfoEx m bir + AccountMigrationV4ToV5 -> \(PBIRV4 bir) -> PBIRV5 <$> V1.migratePersistentBakerInfoEx m bir -- * Conversion diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV0.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV0.hs index 4571a87cb3..303798131b 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV0.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV0.hs @@ -27,6 +27,7 @@ import Lens.Micro.Platform import qualified Concordium.Crypto.SHA256 as Hash import qualified Concordium.Genesis.Data.P4 as P4 +import qualified Concordium.GlobalState.Persistent.Migration as Migration import Concordium.ID.Parameters import Concordium.ID.Types hiding (values) import qualified Concordium.ID.Types as ID @@ -36,7 +37,6 @@ import qualified Concordium.Types.Accounts as BaseAccount hiding (bakerPendingCh import Concordium.Types.Accounts.Releases import Concordium.Types.Execution import Concordium.Types.HashableTo -import qualified Concordium.Types.Migration as Migration import Concordium.GlobalState.Account hiding (addIncomingEncryptedAmount, addToSelfEncryptedAmount, replaceUpTo) import Concordium.GlobalState.BakerInfo (BakerAdd (..), BakerKeyUpdate (..), bakerKeyUpdateToInfo, genesisBakerInfo) diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV1.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV1.hs index e17040bc9a..6d5fd0cf2c 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV1.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Account/StructureV1.hs @@ -33,6 +33,7 @@ import Lens.Micro.Platform import qualified Concordium.Crypto.SHA256 as Hash import Concordium.Genesis.Data import Concordium.GlobalState.Persistent.BlobStore +import Concordium.GlobalState.Persistent.Migration import Concordium.ID.Types hiding (values) import Concordium.Logger import Concordium.Types @@ -117,24 +118,12 @@ migratePersistentBakerInfoEx :: StateMigrationParameters oldpv pv -> PersistentBakerInfoEx (AccountVersionFor oldpv) -> t m (PersistentBakerInfoEx (AccountVersionFor pv)) -migratePersistentBakerInfoEx StateMigrationParametersTrivial = migrateReference return -migratePersistentBakerInfoEx StateMigrationParametersP5ToP6{} = migrateReference return -migratePersistentBakerInfoEx StateMigrationParametersP6ToP7{} = migrateReference migrateBakerInfoExV1 - where - migrateBakerInfoExV1 :: - (AVSupportsDelegation av1, AVSupportsDelegation av2, SupportsValidatorSuspension av2 ~ 'False, Monad m') => - BakerInfoEx av1 -> - m' (BakerInfoEx av2) - migrateBakerInfoExV1 BakerInfoExV1{..} = return BakerInfoExV1{_bieIsSuspended = CFalse, ..} -migratePersistentBakerInfoEx StateMigrationParametersP7ToP8{} = migrateReference migrateBakerInfoExV1 - where - migrateBakerInfoExV1 :: - (AVSupportsDelegation av1, AVSupportsDelegation av2, SupportsValidatorSuspension av2 ~ 'True, Monad m') => - BakerInfoEx av1 -> - m' (BakerInfoEx av2) - migrateBakerInfoExV1 BakerInfoExV1{..} = return BakerInfoExV1{_bieIsSuspended = CTrue False, ..} -migratePersistentBakerInfoEx StateMigrationParametersP8ToP9{} = migrateReference (return . coerceBakerInfoExV1) -migratePersistentBakerInfoEx StateMigrationParametersP9ToP10{} = migrateReference return +migratePersistentBakerInfoEx migration = case accountTypeMigrationFor migration of + AccountMigrationTrivial -> migrateReference return + AccountMigrationV2ToV3 -> migrateReference $ return . coerceBakerInfoExV1 + AccountMigrationV3ToV4 -> migrateReference $ \BakerInfoExV1{..} -> + return BakerInfoExV1{_bieIsSuspended = CTrue False, ..} + AccountMigrationV4ToV5 -> migrateReference $ return . coerceBakerInfoExV1 -- | Migrate a 'V0.PersistentBakerInfoEx' to a 'PersistentBakerInfoEx'. -- See documentation of @migratePersistentBlockState@. @@ -2441,43 +2430,6 @@ migrateV5ToV5 acc = do .. } --- | Migration for 'PersistentAccount'. Supports 'AccountV2', 'AccountV3', 'AccountV4'. --- --- When migrating P6->P7 (account version 2 to 3), the 'AccountMigration' interface is used as --- follows: --- --- * Accounts that previously had a pending change are updated to have a pre-pre-cooldown, and --- 'addAccountInPrePreCooldown' is called. If the pending change is a reduction in stake, --- the reduction is applied immediately to the active stake. If the pending change is a removal, --- the baker or delegator record is removed altogether. --- --- * Accounts that are still delegating but were delegating to a baker for which 'isBakerRemoved' --- returns @True@ are updated to delegate to passive delegation. --- --- * For accounts that are still delegating, 'retainDelegator' is called to record the (new) --- delegation amount and target. -migratePersistentAccount :: - forall m t oldpv pv. - ( IsProtocolVersion oldpv, - SupportMigration m t, - AccountMigration (AccountVersionFor pv) (t m), - AccountStructureVersionFor (AccountVersionFor oldpv) ~ 'AccountStructureV1, - MonadLogger (t m) - ) => - StateMigrationParameters oldpv pv -> - PersistentAccount (AccountVersionFor oldpv) -> - t m (PersistentAccount (AccountVersionFor pv)) -migratePersistentAccount StateMigrationParametersTrivial acc = case accountVersion @(AccountVersionFor oldpv) of - SAccountV2 -> migrateV2ToV2 acc - SAccountV3 -> migrateV3ToV3 acc - SAccountV4 -> migrateV4ToV4 acc - SAccountV5 -> migrateV5ToV5 acc -migratePersistentAccount StateMigrationParametersP5ToP6{} acc = migrateV2ToV2 acc -migratePersistentAccount StateMigrationParametersP6ToP7{} acc = migrateV2ToV3 acc -migratePersistentAccount StateMigrationParametersP7ToP8{} acc = migrateV3ToV4 acc -migratePersistentAccount StateMigrationParametersP8ToP9{} acc = migrateV4ToV5 acc -migratePersistentAccount StateMigrationParametersP9ToP10{} acc = migrateV5ToV5 acc - -- | Migration for 'PersistentAccount' from 'V0.PersistentAccount'. This supports migration from -- 'P4' to 'P5'. migratePersistentAccountFromV0 :: diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Bakers.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Bakers.hs index 94ef038e5b..799018d36e 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Bakers.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Bakers.hs @@ -23,14 +23,16 @@ import Data.Serialize import qualified Data.Vector as Vec import Lens.Micro.Platform +import Concordium.Genesis.Data import qualified Concordium.Genesis.Data.P6 as P6 import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.Parameters import Concordium.GlobalState.Persistent.Account import Concordium.GlobalState.Persistent.BlobStore +import Concordium.GlobalState.Persistent.Migration import Concordium.Types import qualified Concordium.Types.Accounts as BaseAccounts import Concordium.Types.Execution (DelegationTarget (..)) +import Concordium.Types.Parameters import Concordium.Utils.BinarySearch import Concordium.Utils.Serialization @@ -53,7 +55,8 @@ newtype BakerInfos (pv :: ProtocolVersion) -- | See documentation of @migratePersistentBlockState@. migrateBakerInfos :: forall oldpv pv t m. - ( IsProtocolVersion pv, + ( IsProtocolVersion oldpv, + IsProtocolVersion pv, SupportMigration m t ) => StateMigrationParameters oldpv pv -> @@ -158,9 +161,11 @@ migratePersistentEpochBakers migration PersistentEpochBakers{..} = do StateMigrationParametersP7ToP8{} -> SomeParam $ unOParam _bakerFinalizationCommitteeParameters StateMigrationParametersP8ToP9{} -> - SomeParam $ unOParam _bakerFinalizationCommitteeParameters + _bakerFinalizationCommitteeParameters StateMigrationParametersP9ToP10{} -> - SomeParam $ unOParam _bakerFinalizationCommitteeParameters + _bakerFinalizationCommitteeParameters + StateMigrationParametersP10ToP11{} -> + _bakerFinalizationCommitteeParameters return PersistentEpochBakers { _bakerInfos = newBakerInfos, @@ -298,48 +303,29 @@ delegatorTotalCapital f (PersistentActiveDelegatorsV1{..}) = -- In the case of 'StateMigrationParametersP3ToP4', the set of delegators is introduced as empty, -- and the total capital is introduced at 0. migratePersistentActiveDelegators :: + forall oldpv pv t m. (BlobStorable m (), BlobStorable (t m) (), MonadTrans t) => StateMigrationParameters oldpv pv -> PersistentActiveDelegators (AccountVersionFor oldpv) -> t m (PersistentActiveDelegators (AccountVersionFor pv)) -migratePersistentActiveDelegators StateMigrationParametersTrivial = \case - PersistentActiveDelegatorsV0 -> return PersistentActiveDelegatorsV0 - PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP1P2 = \case - PersistentActiveDelegatorsV0 -> return PersistentActiveDelegatorsV0 -migratePersistentActiveDelegators StateMigrationParametersP2P3 = \case - PersistentActiveDelegatorsV0 -> return PersistentActiveDelegatorsV0 -migratePersistentActiveDelegators (StateMigrationParametersP3ToP4 _) = \case - PersistentActiveDelegatorsV0 -> - return - PersistentActiveDelegatorsV1 - { adDelegators = Trie.empty, - adDelegatorTotalCapital = 0 - } -migratePersistentActiveDelegators StateMigrationParametersP4ToP5{} = - \PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP5ToP6{} = \case - PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP6ToP7{} = \case - PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP7ToP8{} = \case - PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP8ToP9{} = \case - PersistentActiveDelegatorsV1{..} -> do - newDelegators <- Trie.migrateTrieN True return adDelegators - return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} -migratePersistentActiveDelegators StateMigrationParametersP9ToP10{} = \case - PersistentActiveDelegatorsV1{..} -> do +migratePersistentActiveDelegators m = case accountTypeMigrationFor m of + AccountMigrationTrivial -> \case + PersistentActiveDelegatorsV0 -> return PersistentActiveDelegatorsV0 + pad@PersistentActiveDelegatorsV1{} -> migrateSimpleV1 pad + AccountMigrationV0ToV1 -> \case + PersistentActiveDelegatorsV0 -> + return + PersistentActiveDelegatorsV1 + { adDelegators = Trie.empty, + adDelegatorTotalCapital = 0 + } + AccountMigrationV1ToV2 -> migrateSimpleV1 + AccountMigrationV2ToV3 -> migrateSimpleV1 + AccountMigrationV3ToV4 -> migrateSimpleV1 + AccountMigrationV4ToV5 -> migrateSimpleV1 + where + migrateSimpleV1 :: (AVSupportsDelegation oldav, AVSupportsDelegation av) => PersistentActiveDelegators oldav -> t m (PersistentActiveDelegators av) + migrateSimpleV1 PersistentActiveDelegatorsV1{..} = do newDelegators <- Trie.migrateTrieN True return adDelegators return PersistentActiveDelegatorsV1{adDelegators = newDelegators, ..} @@ -384,16 +370,13 @@ migrateTotalActiveCapital :: Amount -> TotalActiveCapital (AccountVersionFor oldpv) -> TotalActiveCapital (AccountVersionFor pv) -migrateTotalActiveCapital StateMigrationParametersTrivial _ x = x -migrateTotalActiveCapital StateMigrationParametersP1P2 _ x = x -migrateTotalActiveCapital StateMigrationParametersP2P3 _ x = x -migrateTotalActiveCapital (StateMigrationParametersP3ToP4 _) bts TotalActiveCapitalV0 = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP4ToP5 _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP5ToP6{} _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP6ToP7{} _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP7ToP8{} _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP8ToP9{} _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts -migrateTotalActiveCapital StateMigrationParametersP9ToP10{} _ (TotalActiveCapitalV1 bts) = TotalActiveCapitalV1 bts +migrateTotalActiveCapital m = case accountTypeMigrationFor m of + AccountMigrationTrivial -> \_ tac -> tac + AccountMigrationV0ToV1 -> \bts _ -> TotalActiveCapitalV1 bts + AccountMigrationV1ToV2 -> \_ (TotalActiveCapitalV1 bts) -> TotalActiveCapitalV1 bts + AccountMigrationV2ToV3 -> \_ (TotalActiveCapitalV1 bts) -> TotalActiveCapitalV1 bts + AccountMigrationV3ToV4 -> \_ (TotalActiveCapitalV1 bts) -> TotalActiveCapitalV1 bts + AccountMigrationV4ToV5 -> \_ (TotalActiveCapitalV1 bts) -> TotalActiveCapitalV1 bts instance (IsAccountVersion av) => Serialize (TotalActiveCapital av) where put TotalActiveCapitalV0 = return () diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs index cfd95a875f..2ca810db34 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlobStore.hs @@ -54,6 +54,7 @@ module Concordium.GlobalState.Persistent.BlobStore ( -- * In-memory blob store MemBlobStore (..), newMemBlobStore, + newMemBlobStoreWithBytes, destroyMemBlobStore, MemBlobStoreT (..), @@ -99,6 +100,7 @@ module Concordium.GlobalState.Persistent.BlobStore ( makeHashedBufferedRef, migrateHashedBufferedRef, migrateHashedBufferedRefKeepHash, + getHBRRefIfBlobbed, HashedBufferedRefO, -- ** 'EagerlyHashedBufferedRef' @@ -699,6 +701,10 @@ data MemBlobStore = MemBlobStore newMemBlobStore :: IO MemBlobStore newMemBlobStore = MemBlobStore <$> newMVar LBS.empty <*> newEmptyMVar +-- | Create a new 'MemBlobStore' containing given bytes. +newMemBlobStoreWithBytes :: LBS.ByteString -> IO MemBlobStore +newMemBlobStoreWithBytes bs = MemBlobStore <$> newMVar bs <*> newEmptyMVar + -- | Destroy a 'MemBlobStore'. The caller should ensure that no operations on the 'MemBlobStore' -- can happen after the call to 'destroyMemBlobStore'. destroyMemBlobStore :: MemBlobStore -> IO () @@ -1068,6 +1074,14 @@ makeBufferedRef v = liftIO $ do blobRefToBufferedRef :: BlobRef a -> BufferedRef a blobRefToBufferedRef = BRBlobbed +-- | Get the 'BlobRef' if the value in the reference is stored in the blob store. Else +-- 'Nothing' is returned. +getBRRefIfBlobbed :: + BufferedRef a -> Maybe (BlobRef a) +getBRRefIfBlobbed (BRBlobbed ref) = Just ref +getBRRefIfBlobbed (BRMemory _ _) = Nothing +getBRRefIfBlobbed (BRBoth ref _) = Just ref + instance (Show a) => Show (BufferedRef a) where show (BRBlobbed r) = show r show (BRMemory _ v) = "{" ++ show v ++ "}" @@ -1685,6 +1699,12 @@ makeHashedBufferedRef val = do hashRef <- liftIO $ newIORef Null return $ HashedBufferedRef br hashRef +-- | Get the 'BlobRef' if value in the reference is stored in the blob store. Else +-- 'Nothing' is returned. +getHBRRefIfBlobbed :: + HashedBufferedRef' h a -> Maybe (BlobRef a) +getHBRRefIfBlobbed (HashedBufferedRef br _) = getBRRefIfBlobbed br + instance (DirectBlobStorable m a, MHashableTo m h a) => MHashableTo m h (HashedBufferedRef' h a) where getHashM HashedBufferedRef{..} = liftIO (readIORef bufferedHash) >>= \case diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs index 08e477fdcb..12341ea0f3 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState.hs @@ -7,6 +7,7 @@ {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -64,6 +65,7 @@ import qualified Concordium.GlobalState.Persistent.Accounts as LMDBAccountMap import Concordium.GlobalState.Persistent.Bakers import Concordium.GlobalState.Persistent.BlobStore import qualified Concordium.GlobalState.Persistent.BlockState.Modules as Modules +import qualified Concordium.GlobalState.Persistent.BlockState.Parameters as PCP import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens as PLT import Concordium.GlobalState.Persistent.BlockState.Updates import qualified Concordium.GlobalState.Persistent.Cache as Cache @@ -162,7 +164,7 @@ makeLenses ''PersistentBirkParameters -- - the first epoch trigger block timestamp determined by the state migration data, which -- should be one epoch after the regenesis time. -- --- * P6 to P6: The new seed state is constructed with +-- * Otherwise: The new seed state is constructed with -- - the initial nonce @H.hash $ "Regenesis" <> encode ss1UpdatedNonce@, -- - the epoch reset to 0, -- - the first epoch trigger block time the same as the prior seed state, @@ -174,20 +176,14 @@ migrateSeedState :: StateMigrationParameters oldpv pv -> SeedState (SeedStateVersionFor oldpv) -> SeedState (SeedStateVersionFor pv) -migrateSeedState StateMigrationParametersTrivial{} ss = case ss of - SeedStateV0{} -> ss -- In consensus v0, seed state update is handled prior to migration - SeedStateV1{} -> migrateSeedStateV1Trivial ss -migrateSeedState StateMigrationParametersP1P2{} ss = ss -migrateSeedState StateMigrationParametersP2P3{} ss = ss -migrateSeedState StateMigrationParametersP3ToP4{} ss = ss -migrateSeedState StateMigrationParametersP4ToP5{} ss = ss -migrateSeedState (StateMigrationParametersP5ToP6 (P6.StateMigrationData _ time)) SeedStateV0{..} = - let seed = H.hash $ "Regenesis" <> encode ss0CurrentLeadershipElectionNonce - in initialSeedStateV1 seed time -migrateSeedState StateMigrationParametersP6ToP7{} ss = migrateSeedStateV1Trivial ss -migrateSeedState StateMigrationParametersP7ToP8{} ss = migrateSeedStateV1Trivial ss -migrateSeedState StateMigrationParametersP8ToP9{} ss = migrateSeedStateV1Trivial ss -migrateSeedState StateMigrationParametersP9ToP10{} ss = migrateSeedStateV1Trivial ss +migrateSeedState migration ss = case (ss, sSeedStateVersionFor (protocolVersion @pv)) of + (SeedStateV0{}, SSeedStateVersion0) -> ss + (SeedStateV0{..}, SSeedStateVersion1) -> case migration of + StateMigrationParametersP5ToP6 (P6.StateMigrationData _ time) -> + let seed = H.hash $ "Regenesis" <> encode ss0CurrentLeadershipElectionNonce + in initialSeedStateV1 seed time + (SeedStateV1{}, SSeedStateVersion1) -> migrateSeedStateV1Trivial ss + (SeedStateV1{}, SSeedStateVersion0) -> case migration of {} -- | Trivial migration of a 'SeedStateV1' between protocol versions. migrateSeedStateV1Trivial :: SeedState 'SeedStateVersion1 -> SeedState 'SeedStateVersion1 @@ -592,7 +588,8 @@ migrateBlockRewardDetails :: forall t m oldpv pv. ( MonadBlobStore (t m), MonadTrans t, - SupportsPersistentAccount oldpv m + SupportsPersistentAccount oldpv m, + IsProtocolVersion pv ) => StateMigrationParameters oldpv pv -> -- | Current epoch bakers and stakes, in ascending order of 'BakerId'. @@ -631,20 +628,17 @@ migrateBlockRewardDetails StateMigrationParametersP5ToP6{} _ _ (SomeParam TimePa (BlockRewardDetailsV1 hbr) -> BlockRewardDetailsV1 <$> migrateHashedBufferedRef (migratePoolRewards (rewardPeriodEpochs _tpRewardPeriodLength)) hbr -migrateBlockRewardDetails StateMigrationParametersP6ToP7{} _ _ (SomeParam TimeParametersV1{..}) oldEpoch = \case - (BlockRewardDetailsV1 hbr) -> - BlockRewardDetailsV1 - <$> migrateHashedBufferedRef (migratePoolRewardsP6 oldEpoch _tpRewardPeriodLength) hbr -migrateBlockRewardDetails StateMigrationParametersP7ToP8{} _ _ (SomeParam TimeParametersV1{..}) oldEpoch = \case - (BlockRewardDetailsV1 hbr) -> - BlockRewardDetailsV1 - <$> migrateHashedBufferedRef (migratePoolRewardsP6 oldEpoch _tpRewardPeriodLength) hbr -migrateBlockRewardDetails StateMigrationParametersP8ToP9{} _ _ (SomeParam TimeParametersV1{..}) oldEpoch = \case - (BlockRewardDetailsV1 hbr) -> - BlockRewardDetailsV1 - <$> migrateHashedBufferedRef (migratePoolRewardsP6 oldEpoch _tpRewardPeriodLength) hbr -migrateBlockRewardDetails StateMigrationParametersP9ToP10{} _ _ (SomeParam TimeParametersV1{..}) oldEpoch = \case - (BlockRewardDetailsV1 hbr) -> +migrateBlockRewardDetails migration _ _ (SomeParam TimeParametersV1{..}) oldEpoch = case migration of + StateMigrationParametersP6ToP7{} -> migrateBRD + StateMigrationParametersP7ToP8{} -> migrateBRD + StateMigrationParametersP8ToP9{} -> migrateBRD + StateMigrationParametersP9ToP10{} -> migrateBRD + StateMigrationParametersP10ToP11{} -> migrateBRD + where + migrateBRD :: + (PVSupportsDelegation oldpv, PVSupportsDelegation pv) => + BlockRewardDetails oldpv -> t m (BlockRewardDetails pv) + migrateBRD (BlockRewardDetailsV1 hbr) = BlockRewardDetailsV1 <$> migrateHashedBufferedRef (migratePoolRewardsP6 oldEpoch _tpRewardPeriodLength) hbr @@ -2688,7 +2682,7 @@ doUpdateBakerStake pbs ai newStake = do let curEpoch = bspBirkParameters bsp ^. birkSeedState . epoch upds <- refLoad (bspUpdates bsp) cooldownEpochs <- - (2 +) . _cpBakerExtraCooldownEpochs . _cpCooldownParameters . unStoreSerialized + (2 +) . _cpBakerExtraCooldownEpochs . _cpCooldownParameters . PCP.persistentChainParametersToChainParameters <$> refLoad (currentParameters upds) bakerStakeThreshold <- (^. cpPoolParameters . ppBakerStakeThreshold) <$> doGetChainParameters pbs @@ -2747,7 +2741,7 @@ doRemoveBaker pbs ai = do let curEpoch = bspBirkParameters bsp ^. birkSeedState . epoch upds <- refLoad (bspUpdates bsp) cooldownEpochs <- - (2 +) . _cpBakerExtraCooldownEpochs . _cpCooldownParameters . unStoreSerialized + (2 +) . _cpBakerExtraCooldownEpochs . _cpCooldownParameters . PCP.persistentChainParametersToChainParameters <$> refLoad (currentParameters upds) let updAcc = setAccountStakePendingChange $ @@ -2860,17 +2854,13 @@ doGetRewardStatus pbs = do rsTotalStakedCapital = tc, rsProtocolVersion = demoteProtocolVersion (protocolVersion @pv) } - case protocolVersion @pv of - SP1 -> return rewardsV0 - SP2 -> return rewardsV0 - SP3 -> return rewardsV0 - SP4 -> rewardsV1 - SP5 -> rewardsV1 - SP6 -> rewardsV1 - SP7 -> rewardsV1 - SP8 -> rewardsV1 - SP9 -> rewardsV1 - SP10 -> rewardsV1 + case sSupportsDelegation (sAccountVersionFor (protocolVersion @pv)) of + SFalse -> + -- P1 to P3 + return rewardsV0 + STrue -> + -- P4 onwards + rewardsV1 doRewardFoundationAccount :: (SupportsPersistentState pv m) => PersistentBlockState pv -> Amount -> m (PersistentBlockState pv) doRewardFoundationAccount pbs reward = do @@ -2989,17 +2979,9 @@ doModifyAccount pbs aUpd@AccountUpdate{..} = do doUpd acc = do acc' <- updateAccount aUpd acc releaseChange <- forM _auReleaseSchedule $ \_ -> do - acctRef <- case protocolVersion @pv of - SP1 -> accountCanonicalAddress acc' - SP2 -> accountCanonicalAddress acc' - SP3 -> accountCanonicalAddress acc' - SP4 -> accountCanonicalAddress acc' - SP5 -> return _auIndex - SP6 -> return _auIndex - SP7 -> return _auIndex - SP8 -> return _auIndex - SP9 -> return _auIndex - SP10 -> return _auIndex + acctRef <- case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> accountCanonicalAddress acc' + RSAccountRefTypeAccountIndex -> return _auIndex !oldRel <- accountNextReleaseTimestamp acc !newRel <- accountNextReleaseTimestamp acc' return (acctRef :: RSAccountRef pv, oldRel, newRel) @@ -3544,7 +3526,7 @@ doGetCurrentElectionDifficulty :: doGetCurrentElectionDifficulty pbs = do bsp <- loadPBS pbs upds <- refLoad (bspUpdates bsp) - _cpElectionDifficulty . _cpConsensusParameters . unStoreSerialized <$> refLoad (currentParameters upds) + _cpElectionDifficulty . _cpConsensusParameters . PCP.persistentChainParametersToChainParameters <$> refLoad (currentParameters upds) doGetUpdates :: (SupportsPersistentState pv m) => PersistentBlockState pv -> m (UQ.Updates pv) doGetUpdates = makeBasicUpdates <=< refLoad . bspUpdates <=< loadPBS @@ -3702,17 +3684,9 @@ doProcessReleaseSchedule pbs ts = do Nothing -> error "processReleaseSchedule: scheduled release for invalid account index" return (accs', rs') processAccount :: (Accounts.Accounts pv, ReleaseSchedule pv) -> RSAccountRef pv -> m (Accounts.Accounts pv, ReleaseSchedule pv) - processAccount = case protocolVersion @pv of - SP1 -> processAccountP1 - SP2 -> processAccountP1 - SP3 -> processAccountP1 - SP4 -> processAccountP1 - SP5 -> processAccountP5 - SP6 -> processAccountP5 - SP7 -> processAccountP5 - SP8 -> processAccountP5 - SP9 -> processAccountP5 - SP10 -> processAccountP5 + processAccount = case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> processAccountP1 + RSAccountRefTypeAccountIndex -> processAccountP5 (newAccs, newRS) <- foldM processAccount (bspAccounts bsp, remRS) affectedAccounts storePBS pbs (bsp{bspAccounts = newAccs, bspReleaseSchedule = newRS}) @@ -4361,7 +4335,7 @@ doGetPrePreCooldownAccounts pbs = case sSupportsFlexibleCooldown sav of -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. doSetTokenCirculatingSupply :: forall pv m. - (SupportsPersistentState pv m, PVSupportsPLT pv) => + (SupportsPersistentState pv m, PVSupportsHaskellManagedPLT pv) => PersistentBlockState pv -> PLT.TokenIndex -> PLT.TokenRawAmount -> @@ -4379,7 +4353,7 @@ doSetTokenCirculatingSupply pbs tokIx newSupply = do -- @Nothing@. doCreateToken :: forall pv m. - (SupportsPersistentState pv m, PVSupportsPLT pv) => + (SupportsPersistentState pv m, PVSupportsHaskellManagedPLT pv) => PersistentBlockState pv -> PLT.PLTConfiguration -> m (PLT.TokenIndex, PersistentBlockState pv) @@ -4390,7 +4364,7 @@ doCreateToken pbs tokenConfig = do doSetTokenState :: forall pv m. - (SupportsPersistentState pv m, PVSupportsPLT pv) => + (SupportsPersistentState pv m, PVSupportsHaskellManagedPLT pv) => PersistentBlockState pv -> PLT.TokenIndex -> StateV1.MutableState -> @@ -4621,6 +4595,41 @@ instance (IsProtocolVersion pv, PersistentState av pv r m) => BlockStateQuery (P getCooldownAccounts = doGetCooldownAccounts . hpbsPointers getPreCooldownAccounts = doGetPreCooldownAccounts . hpbsPointers getPrePreCooldownAccounts = doGetPrePreCooldownAccounts . hpbsPointers + getRustPLTBlockState bs = PLT.getRustPLTBlockState . bspProtocolLevelTokens <$> (loadPBS $ hpbsPointers bs) + + liftBlobStore = id + + withUnliftBSQ query = do + -- Construct the context needed for running block state query actions that we unlift + context <- ask + let bscBlobStore = blobStore context + bscLoadCallback = blobLoadCallback context + bscStoreCallback = blobStoreCallback context + pbscBlobStore = BlobStore{..} + + pbscAccountCache = Cache.projectCache context + pbscModuleCache = Cache.projectCache context + + _dbhStoreEnv = context ^. LMDBAccountMap.dbhStoreEnv + _dbhAccountMapStore = context ^. LMDBAccountMap.dbhAccountMapStore + _dbhModuleMapStore = context ^. LMDBAccountMap.dbhModuleMapStore + pbscAccountMap = LMDBAccountMap.DatabaseHandlers{..} + + unliftContext :: PersistentBlockStateContext pv + unliftContext = PersistentBlockStateContext{..} + + -- Extract LogMethod action than can run in IO monad + logMethod <- logEventIO + + -- Run query with the unlift function as argument + let queryIo = query $ \m -> + runLoggerT + ( runReaderT + (runPersistentBlockStateMonad m) + unliftContext + ) + logMethod + liftIO queryIo instance (MonadIO m, PersistentState av pv r m) => ContractStateOperations (PersistentBlockStateMonad pv r m) where thawContractState (Instances.InstanceStateV0 inst) = return inst @@ -4768,10 +4777,52 @@ instance (IsProtocolVersion pv, PersistentState av pv r m) => BlockStateOperatio bsoSetTokenState = doSetTokenState bsoUpdateTokenAccountBalance = doUpdateTokenAccountBalance bsoTouchTokenAccount = doTouchTokenAccount + bsoGetRustPLTBlockState pbs = PLT.getRustPLTBlockState . bspProtocolLevelTokens <$> loadPBS pbs + bsoGetExternalChainParameters pbs = do + bsp <- loadPBS pbs + updates <- refLoad $ bspUpdates bsp + params <- refLoad $ currentParameters updates + case PCP.pcpExternalChainParameters params of + CTrue external -> return external + bsoSetRustPLTBlockState pbs pltState = do + bsp <- loadPBS pbs + storePBS pbs bsp{bspProtocolLevelTokens = PLT.makeRustPLTBlockState pltState} type StateSnapshot (PersistentBlockStateMonad pv r m) = BlockStatePointers pv bsoSnapshotState = loadPBS bsoRollback = storePBS + withUnliftBSO operation = do + -- Construct the context needed for running block state operation actions that we unlift + context <- ask + let bscBlobStore = blobStore context + bscLoadCallback = blobLoadCallback context + bscStoreCallback = blobStoreCallback context + pbscBlobStore = BlobStore{..} + + pbscAccountCache = Cache.projectCache context + pbscModuleCache = Cache.projectCache context + + _dbhStoreEnv = context ^. LMDBAccountMap.dbhStoreEnv + _dbhAccountMapStore = context ^. LMDBAccountMap.dbhAccountMapStore + _dbhModuleMapStore = context ^. LMDBAccountMap.dbhModuleMapStore + pbscAccountMap = LMDBAccountMap.DatabaseHandlers{..} + + unliftContext :: PersistentBlockStateContext pv + unliftContext = PersistentBlockStateContext{..} + + -- Extract LogMethod action than can run in IO monad + logMethod <- logEventIO + + -- Run operation with the unlift function as argument + let operationIo = operation $ \m -> + runLoggerT + ( runReaderT + (runPersistentBlockStateMonad m) + unliftContext + ) + logMethod + liftIO operationIo + instance (IsProtocolVersion pv, PersistentState av pv r m) => BlockStateStorage (PersistentBlockStateMonad pv r m) where thawBlockState = doThawBlockState @@ -4895,20 +4946,16 @@ migrateBlockPointers :: migrateBlockPointers migration BlockStatePointers{..} = do -- We migrate the release schedule first because we may need to access the -- accounts in the process. - let rsMigration = case migration of - StateMigrationParametersTrivial -> trivialReleaseScheduleMigration - StateMigrationParametersP1P2 -> RSMLegacyToLegacy - StateMigrationParametersP2P3 -> RSMLegacyToLegacy - StateMigrationParametersP3ToP4{} -> RSMLegacyToLegacy - StateMigrationParametersP4ToP5{} -> RSMLegacyToNew $ \addr -> - Accounts.getAccountIndex addr bspAccounts <&> \case - Nothing -> error "Account with release schedule does not exist" - Just ai -> ai - StateMigrationParametersP5ToP6{} -> RSMNewToNew - StateMigrationParametersP6ToP7{} -> RSMNewToNew - StateMigrationParametersP7ToP8{} -> RSMNewToNew - StateMigrationParametersP8ToP9{} -> RSMNewToNew - StateMigrationParametersP9ToP10{} -> RSMNewToNew + let rsMigration = case releaseScheduleAccountRefType @oldpv of + RSAccountRefTypeAccountAddress -> case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> RSMLegacyToLegacy + RSAccountRefTypeAccountIndex -> RSMLegacyToNew $ \addr -> + Accounts.getAccountIndex addr bspAccounts <&> \case + Nothing -> error "Account with release schedule does not exist" + Just ai -> ai + RSAccountRefTypeAccountIndex -> case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> case migration of {} + RSAccountRefTypeAccountIndex -> RSMNewToNew logEvent GlobalState LLTrace "Migrating release schedule" newReleaseSchedule <- migrateReleaseSchedule rsMigration bspReleaseSchedule pab <- lift . refLoad $ bspBirkParameters ^. birkActiveBakers @@ -4953,8 +5000,8 @@ migrateBlockPointers migration BlockStatePointers{..} = do nextBakers <- extractBakerStakes =<< refLoad (_birkNextEpochBakers newBirkParameters) -- clear transaction outcomes. let newTransactionOutcomes = emptyTransactionOutcomes (Proxy @pv) - chainParams <- refLoad . currentParameters =<< refLoad newUpdates - let timeParams = _cpTimeParameters . unStoreSerialized $ chainParams + chainParams <- PCP.persistentChainParametersToChainParameters <$> (refLoad . currentParameters =<< refLoad newUpdates) + let timeParams = _cpTimeParameters chainParams logEvent GlobalState LLTrace "Migrating reward details" newRewardDetails <- migrateBlockRewardDetails migration curBakers nextBakers timeParams oldEpoch bspRewardDetails diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ExternalChainParameters.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ExternalChainParameters.hs new file mode 100644 index 0000000000..ad80e96d88 --- /dev/null +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ExternalChainParameters.hs @@ -0,0 +1,199 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Bindings to the Rust external chain-parameters implementation. +-- +-- The component stores node-internal chain-parameter state whose public query +-- representation is assembled by the node. It is deliberately separate from +-- the public/wire chain-parameter types in @concordium-base@. +module Concordium.GlobalState.Persistent.BlockState.ExternalChainParameters ( + RustExternalChainParameters, + ForeignExternalChainParametersPtr, + wrapFFIPtr, + p11NewExternalChainParameters, + withExternalChainParameters, + ExternalChainParametersHash (..), + executeChainUpdate, + getMaxLockDuration, +) where + +import Control.Monad.Trans (liftIO) +import qualified Data.ByteString.Unsafe as BS +import qualified Data.Serialize as S +import qualified Data.Word as Word +import qualified Foreign as FFI +import qualified Foreign.C.Types as FFI + +import Concordium.Common.Time (Duration (..)) +import qualified Concordium.Crypto.SHA256 as SHA256 +import qualified Concordium.Types as Types +import qualified Concordium.Types.HashableTo as Hashable +import qualified Concordium.Types.Updates as Updates +import qualified Control.Monad as Monad +import qualified Data.FixedByteString as FixedByteString + +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore + +-- | Opaque type representing Rust-maintained external chain parameters. +-- The value is allocated and deallocated in Rust. +data RustExternalChainParameters + +-- | Opaque pointer to immutable external chain parameters managed by Rust. +-- +-- Memory is deallocated using a finalizer. +newtype ForeignExternalChainParametersPtr (pv :: Types.ProtocolVersion) = ForeignExternalChainParametersPtr (FFI.ForeignPtr RustExternalChainParameters) + +-- | Convert a raw pointer returned by Rust into a managed pointer. +wrapFFIPtr :: FFI.Ptr RustExternalChainParameters -> IO (ForeignExternalChainParametersPtr pv) +wrapFFIPtr paramsPtr = ForeignExternalChainParametersPtr <$> FFI.newForeignPtr ffiFreeExternalChainParameters paramsPtr + +-- | Deallocate a pointer to external chain parameters. +foreign import ccall unsafe "&ffi_free_external_chain_parameters" + ffiFreeExternalChainParameters :: FFI.FinalizerPtr RustExternalChainParameters + +-- | Get temporary access to the external chain-parameters pointer. +-- +-- The pointer must not be leaked from the computation. +withExternalChainParameters :: ForeignExternalChainParametersPtr pv -> (FFI.Ptr RustExternalChainParameters -> IO a) -> IO a +withExternalChainParameters (ForeignExternalChainParametersPtr foreignPtr) = FFI.withForeignPtr foreignPtr + +-- | Allocate new P11 external chain parameters with an initial maximum lock duration. +p11NewExternalChainParameters :: (BlobStore.MonadBlobStore m) => Duration -> m (ForeignExternalChainParametersPtr 'Types.P11) +p11NewExternalChainParameters (Duration maxLockDuration) = liftIO $ do + FFI.alloca $ \paramsDestPtr -> do + status <- ffiP11NewExternalChainParameters maxLockDuration paramsDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when creating P11 external chain parameters" + params <- FFI.peek paramsDestPtr + wrapFFIPtr params + +foreign import ccall "ffi_p11_new_external_chain_parameters" + ffiP11NewExternalChainParameters :: + FFI.Word64 -> + FFI.Ptr (FFI.Ptr RustExternalChainParameters) -> + IO FFI.Word8 + +instance (BlobStore.MonadBlobStore m, Types.IsProtocolVersion pv) => BlobStore.BlobStorable m (ForeignExternalChainParametersPtr pv) where + load = do + blobRef <- S.get + pure $! do + loadCallback <- fst <$> BlobStore.getCallbacks + liftIO $! do + FFI.alloca $ \paramsDestPtr -> do + status <- + ffiLoadExternalChainParameters + loadCallback + blobRef + (Types.protocolVersionToWord64 $ Types.demoteProtocolVersion $ Types.protocolVersion @pv) + paramsDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when loading external chain parameters" + params <- FFI.peek paramsDestPtr + wrapFFIPtr params + storeUpdate params = do + storeCallback <- snd <$> BlobStore.getCallbacks + blobRef <- liftIO $ FFI.alloca $ \blobRefDestPtr -> do + status <- withExternalChainParameters params $ ffiStoreExternalChainParameters storeCallback blobRefDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when storing external chain parameters" + BlobStore.BlobRef @RustExternalChainParameters <$> FFI.peek blobRefDestPtr + return (S.put blobRef, params) + +foreign import ccall "ffi_load_external_chain_parameters" + ffiLoadExternalChainParameters :: + FFI.LoadCallback -> + BlobStore.BlobRef RustExternalChainParameters -> + FFI.Word64 -> + FFI.Ptr (FFI.Ptr RustExternalChainParameters) -> + IO FFI.Word8 + +foreign import ccall "ffi_store_external_chain_parameters" + ffiStoreExternalChainParameters :: + FFI.StoreCallback -> + FFI.Ptr FFI.Word64 -> + FFI.Ptr RustExternalChainParameters -> + IO FFI.Word8 + +instance (BlobStore.MonadBlobStore m) => BlobStore.Cacheable m (ForeignExternalChainParametersPtr pv) where + cache params = do + loadCallback <- fst <$> BlobStore.getCallbacks + status <- liftIO $! withExternalChainParameters params (ffiCacheExternalChainParameters loadCallback) + Monad.unless (status == 0) $ error "Unexpected panic when caching external chain parameters" + return params + +foreign import ccall "ffi_cache_external_chain_parameters" + ffiCacheExternalChainParameters :: + FFI.LoadCallback -> + FFI.Ptr RustExternalChainParameters -> + IO FFI.Word8 + +-- | The hash of external chain parameters. +newtype ExternalChainParametersHash = ExternalChainParametersHash {theExternalChainParametersHash :: SHA256.Hash} + deriving newtype (Eq, Ord, Show, S.Serialize) + +instance (BlobStore.MonadBlobStore m) => Hashable.MHashableTo m ExternalChainParametersHash (ForeignExternalChainParametersPtr pv) where + getHashM params = do + loadCallback <- fst <$> BlobStore.getCallbacks + ((), hash) <- + liftIO $ + withExternalChainParameters params $ \paramsPtr -> + FixedByteString.createWith $ \hashDestPtr -> do + status <- ffiHashExternalChainParameters loadCallback paramsPtr hashDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when hashing external chain parameters" + return $ ExternalChainParametersHash (SHA256.Hash hash) + +foreign import ccall "ffi_hash_external_chain_parameters" + ffiHashExternalChainParameters :: + FFI.LoadCallback -> + FFI.Ptr RustExternalChainParameters -> + FFI.Ptr FFI.Word8 -> + IO FFI.Word8 + +-- | Execute a chain update against external chain parameters. +-- +-- The current parameters are left unchanged. On success, this returns a newly +-- allocated successor state. An unexpected payload or a Rust panic terminates +-- execution with an error. +-- +-- @ +-- updated <- executeChainUpdate current (Updates.MaxLockDurationUpdatePayload duration) +-- @ +executeChainUpdate :: ForeignExternalChainParametersPtr pv -> Updates.UpdatePayload -> IO (ForeignExternalChainParametersPtr pv) +executeChainUpdate params payload = do + let payloadBytes = S.runPut $ Updates.putUpdatePayload payload + FFI.alloca $ \paramsDestPtr -> do + status <- + withExternalChainParameters params $ \paramsPtr -> + BS.unsafeUseAsCStringLen payloadBytes $ \(payloadPtr, payloadLen) -> + ffiExecuteExternalChainParametersUpdate + paramsPtr + (FFI.castPtr payloadPtr) + (fromIntegral payloadLen) + paramsDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when executing an external chain parameter update" + FFI.peek paramsDestPtr >>= wrapFFIPtr + +foreign import ccall "ffi_execute_external_chain_parameters_update" + ffiExecuteExternalChainParametersUpdate :: + FFI.Ptr RustExternalChainParameters -> + FFI.Ptr Word.Word8 -> + FFI.CSize -> + FFI.Ptr (FFI.Ptr RustExternalChainParameters) -> + IO FFI.Word8 + +-- | Read the current maximum lock duration from external chain parameters. +getMaxLockDuration :: ForeignExternalChainParametersPtr pv -> IO Duration +getMaxLockDuration params = + withExternalChainParameters params $ \paramsPtr -> + FFI.alloca $ \durationPtr -> do + status <- ffiGetExternalChainParametersMaxLockDuration paramsPtr durationPtr + Monad.unless (status == 0) $ error "Unexpected panic when reading max lock duration" + Duration <$> FFI.peek durationPtr + +foreign import ccall "ffi_get_external_chain_parameters_max_lock_duration" + ffiGetExternalChainParametersMaxLockDuration :: + FFI.Ptr RustExternalChainParameters -> + FFI.Ptr FFI.Word64 -> + IO FFI.Word8 diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs index bea304ccdd..993e8b769d 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Modules.hs @@ -578,7 +578,7 @@ migrateModules migration mods = do StateMigrationParametersP7ToP8{} -> return $! moduleVInterface{GSWasm.miModule = PIMVMem artifact} StateMigrationParametersP8ToP9{} -> return $! moduleVInterface{GSWasm.miModule = PIMVMem artifact} StateMigrationParametersP9ToP10{} -> return $! moduleVInterface{GSWasm.miModule = PIMVMem artifact} - + StateMigrationParametersP10ToP11{} -> return $! moduleVInterface{GSWasm.miModule = PIMVMem artifact} -- store the module into the new state, and remove it from memory makeFlushedHashedCachedRef $! mkModule (getWasmVersion @v) $! diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Parameters.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Parameters.hs new file mode 100644 index 0000000000..cb65291802 --- /dev/null +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Parameters.hs @@ -0,0 +1,281 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE EmptyCase #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Node-owned persistent chain parameters. +-- +-- This type is the persistent node representation used by the update state. It +-- is distinct from the @concordium-base@ public/wire @ChainParameters'@ view. +-- The aggregate public/wire type is only used at conversion boundaries; the +-- persistent storage model has its own record fields and a +-- Rust-managed external chain-parameters pointer. +module Concordium.GlobalState.Persistent.BlockState.Parameters ( + PersistentChainParameters (..), + makePersistentChainParameters, + persistentChainParametersToChainParameters, + persistentChainParametersToChainParametersM, + updateChainParameters, + executeExternalChainParameterUpdate, +) where + +import Control.Monad.IO.Class +import qualified Data.ByteString as BS +import qualified Data.Serialize as S + +import qualified Concordium.Crypto.SHA256 as H +import Concordium.GlobalState.Persistent.BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.ExternalChainParameters as ECP +import Concordium.Types +import Concordium.Types.Conditionally +import Concordium.Types.HashableTo +import Concordium.Types.Parameters +import qualified Concordium.Types.Updates as Updates + +-- | Persistent node-owned chain parameters. +data PersistentChainParameters (pv :: ProtocolVersion) = PersistentChainParameters + { -- | Consensus parameters. + pcpConsensusParameters :: !(ConsensusParameters (ChainParametersVersionFor pv)), + -- | Exchange rates. + pcpExchangeRates :: !ExchangeRates, + -- | Cooldown parameters. + pcpCooldownParameters :: !(CooldownParameters (ChainParametersVersionFor pv)), + -- | Time parameters. + pcpTimeParameters :: !(OParam 'PTTimeParameters (ChainParametersVersionFor pv) TimeParameters), + -- | LimitAccountCreation: the maximum number of accounts that may be created in one block. + pcpAccountCreationLimit :: !CredentialsPerBlockLimit, + -- | Reward parameters. + pcpRewardParameters :: !(RewardParameters (ChainParametersVersionFor pv)), + -- | Foundation account index. + pcpFoundationAccount :: !AccountIndex, + -- | Parameters for baker pools. + pcpPoolParameters :: !(PoolParameters (ChainParametersVersionFor pv)), + -- | Finalization committee parameters. + pcpFinalizationCommitteeParameters :: !(OParam 'PTFinalizationCommitteeParameters (ChainParametersVersionFor pv) FinalizationCommitteeParameters), + -- | Validator score parameters. + pcpValidatorScoreParameters :: !(OParam 'PTValidatorScoreParameters (ChainParametersVersionFor pv) ValidatorScoreParameters), + -- | Rust-managed external chain parameters, present when supported by the protocol. + pcpExternalChainParameters :: !(Conditionally (SupportsRustManagedECP pv) (ECP.ForeignExternalChainParametersPtr pv)) + } + +-- | Convert a public/wire chain-parameter view and external pointer into the +-- persistent node representation. +fromChainParameters :: + ChainParameters pv -> + Conditionally (SupportsRustManagedECP pv) (ECP.ForeignExternalChainParametersPtr pv) -> + PersistentChainParameters pv +fromChainParameters ChainParameters{..} pcpExternalChainParameters = + PersistentChainParameters + { pcpConsensusParameters = _cpConsensusParameters, + pcpExchangeRates = _cpExchangeRates, + pcpCooldownParameters = _cpCooldownParameters, + pcpTimeParameters = _cpTimeParameters, + pcpAccountCreationLimit = _cpAccountCreationLimit, + pcpRewardParameters = _cpRewardParameters, + pcpFoundationAccount = _cpFoundationAccount, + pcpPoolParameters = _cpPoolParameters, + pcpFinalizationCommitteeParameters = _cpFinalizationCommitteeParameters, + pcpValidatorScoreParameters = _cpValidatorScoreParameters, + pcpExternalChainParameters = pcpExternalChainParameters + } + +-- | Construct persistent chain parameters from the public/wire view. +makePersistentChainParameters :: + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + ChainParameters pv -> + m (PersistentChainParameters pv) +makePersistentChainParameters chainParameters = do + externalChainParameters <- makeInitialExternalChainParameters @m @pv chainParameters + return $ fromChainParameters chainParameters externalChainParameters + +-- | Construct initial external chain parameters from the public chain-parameter view. +makeInitialExternalChainParameters :: + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + ChainParameters pv -> + m (Conditionally (SupportsRustManagedECP pv) (ECP.ForeignExternalChainParametersPtr pv)) +makeInitialExternalChainParameters chainParameters = case protocolVersion @pv of + SP1 -> return CFalse + SP2 -> return CFalse + SP3 -> return CFalse + SP4 -> return CFalse + SP5 -> return CFalse + SP6 -> return CFalse + SP7 -> return CFalse + SP8 -> return CFalse + SP9 -> return CFalse + SP10 -> return CFalse + SP11 -> + CTrue <$> case _cpMaxLockDuration chainParameters of + SomeParam (Just duration) -> ECP.p11NewExternalChainParameters duration + SomeParam Nothing -> error "P11 external chain parameters require max lock duration" + +-- | Placeholder public-view value for the max-lock-duration field. +-- +-- The authoritative P11 value is in the external chain-parameters component. +maxLockDurationPlaceholder :: SChainParametersVersion cpv -> OParam 'PTMaxLockDuration cpv (Maybe Duration) +maxLockDurationPlaceholder = \case + SChainParametersV0 -> NoParam + SChainParametersV1 -> NoParam + SChainParametersV2 -> NoParam + SChainParametersV3 -> SomeParam Nothing + +-- | Convert persistent chain parameters to the public/wire view, using the +-- placeholder external fields. +persistentChainParametersToChainParameters :: + forall pv. + (IsProtocolVersion pv) => + PersistentChainParameters pv -> + ChainParameters pv +persistentChainParametersToChainParameters params = + makeChainParametersView params (maxLockDurationPlaceholder (chainParametersVersion @(ChainParametersVersionFor pv))) + +-- | Convert persistent chain parameters to the public/wire view, sourcing +-- externally-managed fields from the external chain-parameters component when present. +persistentChainParametersToChainParametersM :: + forall m pv. + (MonadIO m, IsProtocolVersion pv) => + PersistentChainParameters pv -> + m (ChainParameters pv) +persistentChainParametersToChainParametersM params@PersistentChainParameters{..} = do + maxLockDuration <- case pcpExternalChainParameters of + CFalse -> return $ maxLockDurationPlaceholder (chainParametersVersion @(ChainParametersVersionFor pv)) + CTrue external -> case chainParametersVersion @(ChainParametersVersionFor pv) of + SChainParametersV3 -> do + duration <- liftIO $ ECP.getMaxLockDuration external + return $ SomeParam (Just duration) + _ -> return $ maxLockDurationPlaceholder (chainParametersVersion @(ChainParametersVersionFor pv)) + return $ makeChainParametersView params maxLockDuration + +-- | Construct the public/wire view from persistent fields and a supplied +-- max-lock-duration value. +makeChainParametersView :: + PersistentChainParameters pv -> + OParam 'PTMaxLockDuration (ChainParametersVersionFor pv) (Maybe Duration) -> + ChainParameters pv +makeChainParametersView PersistentChainParameters{..} maxLockDuration = + ChainParameters + { _cpConsensusParameters = pcpConsensusParameters, + _cpExchangeRates = pcpExchangeRates, + _cpCooldownParameters = pcpCooldownParameters, + _cpTimeParameters = pcpTimeParameters, + _cpAccountCreationLimit = pcpAccountCreationLimit, + _cpRewardParameters = pcpRewardParameters, + _cpFoundationAccount = pcpFoundationAccount, + _cpPoolParameters = pcpPoolParameters, + _cpFinalizationCommitteeParameters = pcpFinalizationCommitteeParameters, + _cpValidatorScoreParameters = pcpValidatorScoreParameters, + _cpMaxLockDuration = maxLockDuration + } + +-- | Update the Haskell-managed chain parameters while preserving any +-- Rust-managed external chain-parameters pointer. +updateChainParameters :: + ChainParameters pv -> + PersistentChainParameters pv -> + PersistentChainParameters pv +updateChainParameters newChainParameters PersistentChainParameters{..} = + fromChainParameters newChainParameters pcpExternalChainParameters + +-- | Execute a chain update against the Rust-managed external chain parameters. +-- +-- The current parameters are left unchanged. An unexpected payload, absence of +-- external chain parameters, or a Rust panic terminates execution with an error. +-- +-- @ +-- updated <- executeExternalChainParameterUpdate (Updates.MaxLockDurationUpdatePayload duration) current +-- @ +executeExternalChainParameterUpdate :: + (MonadIO m) => + Updates.UpdatePayload -> + PersistentChainParameters pv -> + m (PersistentChainParameters pv) +executeExternalChainParameterUpdate payload params@PersistentChainParameters{pcpExternalChainParameters = CTrue external} = do + newExternal <- liftIO $ ECP.executeChainUpdate external payload + return params{pcpExternalChainParameters = CTrue newExternal} +executeExternalChainParameterUpdate _ PersistentChainParameters{pcpExternalChainParameters = CFalse} = + error "External chain parameter updates require external chain parameters" + +-- | Serialize persistent chain parameters. +putPersistentChainParameters :: forall pv. (IsProtocolVersion pv) => S.Putter (PersistentChainParameters pv) +putPersistentChainParameters PersistentChainParameters{..} = do + withIsConsensusParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) $ S.put pcpConsensusParameters + S.put pcpExchangeRates + putCooldownParameters pcpCooldownParameters + S.put pcpTimeParameters + S.put pcpAccountCreationLimit + S.put pcpRewardParameters + S.put pcpFoundationAccount + putPoolParameters pcpPoolParameters + S.put pcpFinalizationCommitteeParameters + S.put pcpValidatorScoreParameters + +-- | Deserialize persistent chain parameters, excluding the external component. +-- +-- This is an internal helper function +getPersistentChainParametersFields :: forall pv. (IsProtocolVersion pv) => S.Get (ChainParameters pv) +getPersistentChainParametersFields = do + _cpConsensusParameters <- withIsConsensusParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) S.get + _cpExchangeRates <- S.get + _cpCooldownParameters <- withIsCooldownParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) S.get + _cpTimeParameters <- S.get + _cpAccountCreationLimit <- S.get + _cpRewardParameters <- S.get + _cpFoundationAccount <- S.get + _cpPoolParameters <- withIsPoolParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) S.get + _cpFinalizationCommitteeParameters <- S.get + _cpValidatorScoreParameters <- S.get + let _cpMaxLockDuration = maxLockDurationPlaceholder (chainParametersVersion @(ChainParametersVersionFor pv)) + return ChainParameters{..} + +instance + (MonadBlobStore m, IsProtocolVersion pv) => + BlobStorable m (PersistentChainParameters pv) + where + storeUpdate params@PersistentChainParameters{..} = do + (pExternal :: S.Put, external') <- case pcpExternalChainParameters of + CFalse -> return (return (), CFalse) + CTrue external -> do + (putExternal, external') <- storeUpdate external + return (putExternal, CTrue external') + let newParams = params{pcpExternalChainParameters = external'} + return + ( do + putPersistentChainParameters params + pExternal, + newParams + ) + load = do + chainParameters <- getPersistentChainParametersFields @pv + mExternal <- conditionallyA (sSupportsRustManagedECP (protocolVersion @pv)) load + return $ do + externalChainParameters <- sequenceA mExternal + return $ fromChainParameters chainParameters externalChainParameters + +instance + (MonadBlobStore m) => + Cacheable m (PersistentChainParameters pv) + where + cache params@PersistentChainParameters{..} = do + external' <- traverse cache pcpExternalChainParameters + return params{pcpExternalChainParameters = external'} + +instance + (MonadBlobStore m, IsProtocolVersion pv) => + MHashableTo m H.Hash (PersistentChainParameters pv) + where + getHashM params@PersistentChainParameters{..} = do + hExternal <- traverse (getHashM @_ @ECP.ExternalChainParametersHash) pcpExternalChainParameters + return $ + H.hash $ + S.runPut (putPersistentChainParameters params) + <> externalHashBytes hExternal + where + externalHashBytes :: Conditionally b ECP.ExternalChainParametersHash -> BS.ByteString + externalHashBytes = \case + CFalse -> mempty + CTrue (ECP.ExternalChainParametersHash h) -> H.hashToByteString h diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens.hs index f2397f2b2c..364d0139dd 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens.hs @@ -4,16 +4,23 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +-- We suppress redundant constraint warnings since GHC does not detect when a constraint is used +-- for pattern matching. (See: https://gitlab.haskell.org/ghc/ghc/-/issues/20896) +{-# OPTIONS_GHC -Wno-redundant-constraints #-} +-- | Implementation of protocol-level tokens block state. This is both the Haskell implementation (V0) +-- and the Rust implementation (V1). The latter is using bindings to the Rust library which implements the PLT block state. module Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens where +import Control.Arrow +import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Class import Data.Bits -import Data.Bool.Singletons import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC import qualified Data.ByteString.Short as SBS import Data.Char (toUpper) +import Data.Coerce (coerce) import qualified Data.Map.Strict as Map import Data.Serialize import Data.Word @@ -29,8 +36,8 @@ import Concordium.Utils import Concordium.GlobalState.Basic.BlockState.LFMBTree (LFMBTreeHash' (..)) import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import Concordium.GlobalState.Persistent.BlobStore +import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState as RustBS import qualified Concordium.GlobalState.Persistent.LFMBTree as LFMBTree -import Control.Monad.IO.Class (liftIO) -- | A token index is the index of a token in the 'ProtocolLevelTokens' table. newtype TokenIndex = TokenIndex {theTokenIndex :: Word64} @@ -168,11 +175,6 @@ instance (MonadBlobStore m) => Cacheable m ProtocolLevelTokens where pltTable' <- cache _pltTable return ProtocolLevelTokens{_pltTable = pltTable', ..} --- | The hash of a 'ProtocolLevelTokens'. This is the hash of the LFMBTree holding the --- 'PLT's. The hash is computed using the 'BlockHashVersion1' algorithm. -newtype ProtocolLevelTokensHash = ProtocolLevelTokensHash {theProtocolLevelTokensHash :: SHA256.Hash} - deriving newtype (Eq, Ord, Show, Serialize) - instance (MonadBlobStore m) => MHashableTo m ProtocolLevelTokensHash ProtocolLevelTokens where getHashM ProtocolLevelTokens{..} = ProtocolLevelTokensHash . theLFMBTreeHash @BlockHashVersion1 @@ -186,80 +188,96 @@ emptyProtocolLevelTokens = _pltMap = Map.empty } --- | Protocol level tokens where supported by the protocol version. --- The 'ProtocolLevelTokens' structure is stored under a 'HashedBufferedRef''. -newtype ProtocolLevelTokensForPV (pv :: ProtocolVersion) = ProtocolLevelTokensForPV - { theProtocolLevelTokensForPV :: - (Conditionally (SupportsPLT (AccountVersionFor pv))) - (HashedBufferedRef' ProtocolLevelTokensHash ProtocolLevelTokens) - } - -instance (MonadBlobStore m, IsProtocolVersion pv) => BlobStorable m (ProtocolLevelTokensForPV pv) where - load = case sSupportsPLT (accountVersion @(AccountVersionFor pv)) of - SFalse -> return (return (ProtocolLevelTokensForPV CFalse)) - STrue -> fmap (ProtocolLevelTokensForPV . CTrue) <$> load - storeUpdate pltPV@(ProtocolLevelTokensForPV CFalse) = do - return (return (), pltPV) - storeUpdate (ProtocolLevelTokensForPV (CTrue pltRef)) = do - (ppltRef, pltRef') <- storeUpdate pltRef - return (ppltRef, ProtocolLevelTokensForPV (CTrue pltRef')) +-- | Protocol level tokens depending on PLT state version. +-- +-- * 'PLTStateNone': No PLT state +-- * 'PLTStateV0': Managed in Haskell +-- * 'PLTStateV1': Managed in Rust +data ProtocolLevelTokensForPV (pv :: ProtocolVersion) where + ProtocolLevelTokensNone :: + (PltStateVersionFor pv ~ 'PLTStateNone) => + ProtocolLevelTokensForPV pv + ProtocolLevelTokensV0 :: + (PltStateVersionFor pv ~ 'PLTStateV0) => + (HashedBufferedRef' ProtocolLevelTokensHash ProtocolLevelTokens) -> ProtocolLevelTokensForPV pv + ProtocolLevelTokensV1 :: + (PltStateVersionFor pv ~ 'PLTStateV1) => + RustBS.ForeignPLTBlockStatePtr pv -> ProtocolLevelTokensForPV pv instance - (MonadBlobStore m, b ~ SupportsPLT (AccountVersionFor pv)) => - MHashableTo m (Conditionally b ProtocolLevelTokensHash) (ProtocolLevelTokensForPV pv) + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + BlobStorable m (ProtocolLevelTokensForPV pv) where - getHashM (ProtocolLevelTokensForPV CFalse) = return CFalse - getHashM (ProtocolLevelTokensForPV (CTrue ref)) = CTrue <$> getHashM ref + load = case pltStateVersion @(PltStateVersionFor pv) of + SPLTStateNone -> return $ return ProtocolLevelTokensNone + SPLTStateV0 -> fmap ProtocolLevelTokensV0 <$> load + SPLTStateV1 -> fmap ProtocolLevelTokensV1 <$> load + storeUpdate ProtocolLevelTokensNone = return (return (), ProtocolLevelTokensNone) + storeUpdate (ProtocolLevelTokensV0 hbref) = second ProtocolLevelTokensV0 <$> storeUpdate hbref + storeUpdate (ProtocolLevelTokensV1 fstate) = second ProtocolLevelTokensV1 <$> storeUpdate fstate + +instance (MonadBlobStore m) => Cacheable m (ProtocolLevelTokensForPV pv) where + cache = \case + ProtocolLevelTokensNone -> return ProtocolLevelTokensNone + ProtocolLevelTokensV0 hbref -> ProtocolLevelTokensV0 <$> cache hbref + ProtocolLevelTokensV1 fstate -> ProtocolLevelTokensV1 <$> cache fstate instance - (MonadBlobStore m, PVSupportsPLT pv) => - MHashableTo m ProtocolLevelTokensHash (ProtocolLevelTokensForPV pv) + (MonadBlobStore m, b ~ PltStatePresent (PltStateVersionFor pv)) => + MHashableTo m (Conditionally b ProtocolLevelTokensHash) (ProtocolLevelTokensForPV pv) where - getHashM (ProtocolLevelTokensForPV (CTrue ref)) = getHashM ref + getHashM = \case + ProtocolLevelTokensNone -> return CFalse + ProtocolLevelTokensV0 hbref -> CTrue <$> getHashM hbref + ProtocolLevelTokensV1 fstate -> CTrue <$> getHashM fstate -instance (MonadBlobStore m) => Cacheable m (ProtocolLevelTokensForPV pv) where - cache (ProtocolLevelTokensForPV (CTrue pltsRef)) = - ProtocolLevelTokensForPV . CTrue <$> cache pltsRef - cache pvPLTs = pure pvPLTs - --- | Load a 'ProtocolLevelTokens' from a 'ProtocolLevelTokensForPV'. +-- | Load a 'ProtocolLevelTokens' in the Haskell managed version of 'ProtocolLevelTokensForPV'. loadPLTs :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => ProtocolLevelTokensForPV pv -> m ProtocolLevelTokens -loadPLTs = refLoad . uncond . theProtocolLevelTokensForPV +loadPLTs (ProtocolLevelTokensV0 hbref) = refLoad hbref --- | Store a 'ProtocolLevelTokens' in a 'ProtocolLevelTokensForPV'. +-- | Store a 'ProtocolLevelTokens' in the Haskell managed version of 'ProtocolLevelTokensForPV'. storePLTs :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => ProtocolLevelTokens -> m (ProtocolLevelTokensForPV pv) -storePLTs = fmap (ProtocolLevelTokensForPV . CTrue) . refMake +storePLTs = fmap ProtocolLevelTokensV0 . refMake --- | Store a 'ProtocolLevelTokens' in a 'ProtocolLevelTokensForPV' if the protocol version supports --- protocol-level tokens. -conditionallyStorePLTs :: - forall m pv. - (IsProtocolVersion pv, MonadBlobStore m) => - ProtocolLevelTokens -> - m (ProtocolLevelTokensForPV pv) -conditionallyStorePLTs = case sSupportsPLT (accountVersion @(AccountVersionFor pv)) of - STrue -> storePLTs - SFalse -> const (return $ ProtocolLevelTokensForPV CFalse) +-- | Get 'ForeignPLTBlockStatePtr' in the Rust managed version of 'ProtocolLevelTokensForPV'. +getRustPLTBlockState :: + (PltStateVersionFor pv ~ 'PLTStateV1) => + ProtocolLevelTokensForPV pv -> + RustBS.ForeignPLTBlockStatePtr pv +getRustPLTBlockState (ProtocolLevelTokensV1 state) = state + +-- | Set 'ForeignPLTBlockStatePtr' in the Rust managed version of 'ProtocolLevelTokensForPV'. +makeRustPLTBlockState :: + (PltStateVersionFor pv ~ 'PLTStateV1) => + RustBS.ForeignPLTBlockStatePtr pv -> + ProtocolLevelTokensForPV pv +makeRustPLTBlockState = ProtocolLevelTokensV1 -- | An empty 'ProtocolLevelTokensForPV' with no tokens. emptyProtocolLevelTokensForPV :: - (IsProtocolVersion pv, MonadBlobStore m) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => m (ProtocolLevelTokensForPV pv) -emptyProtocolLevelTokensForPV = conditionallyStorePLTs emptyProtocolLevelTokens +emptyProtocolLevelTokensForPV = case pltStateVersion @(PltStateVersionFor pv) of + SPLTStateNone -> return $ ProtocolLevelTokensNone + SPLTStateV0 -> storePLTs emptyProtocolLevelTokens + SPLTStateV1 -> ProtocolLevelTokensV1 <$> RustBS.empty -- | Get the list of all existing protocol-level tokens by their 'TokenId's. -- This returns the empty list when the protocol version does not support PLTs. -getPLTList :: (MonadBlobStore m) => ProtocolLevelTokensForPV pv -> m [TokenId] -getPLTList (ProtocolLevelTokensForPV CFalse) = return [] -getPLTList (ProtocolLevelTokensForPV (CTrue plts)) = do - table <- _pltTable <$> refLoad plts - LFMBTree.mfold step [] table +-- +-- This implementation is for the Haskell managed state version V0. +getPLTList :: (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => ProtocolLevelTokensForPV pv -> m [TokenId] +getPLTList pltsV0 = do + plts <- loadPLTs pltsV0 + LFMBTree.mfold step [] (_pltTable plts) where step acc v = do tid <- _pltTokenId <$> refLoad (_pltConfiguration v) @@ -267,8 +285,10 @@ getPLTList (ProtocolLevelTokensForPV (CTrue plts)) = do -- | Get the 'TokenIndex' for a 'TokenId'. Returns @Nothing@ if there is no token with the given -- 'TokenId'. +-- +-- This implementation is for the Haskell managed state version V0. getTokenIndex :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenId -> ProtocolLevelTokensForPV pv -> m (Maybe TokenIndex) @@ -278,14 +298,16 @@ getTokenIndex tokId = fmap (Map.lookup ntid . _pltMap) . loadPLTs -- | Get the 'PLT' with the given 'TokenIndex'. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. lookupPLT :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> ProtocolLevelTokensForPV pv -> m PLT -lookupPLT index pvPLTs = do - plts <- loadPLTs pvPLTs +lookupPLT index pltsV0 = do + plts <- loadPLTs pltsV0 mPLT <- LFMBTree.lookup index (_pltTable plts) case mPLT of Just plt -> return plt @@ -295,29 +317,33 @@ lookupPLT index pvPLTs = do -- | Create a mutable state from a persistent one. This is generative, it creates independent -- mutable states in different calls. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. getMutableTokenState :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> ProtocolLevelTokensForPV pv -> m StateV1.MutableState -getMutableTokenState index pvPLTs = do - plt <- lookupPLT index pvPLTs +getMutableTokenState index pltsV0 = do + plt <- lookupPLT index pltsV0 loadCallback <- fst <$> getCallbacks liftIO $ StateV1.thaw loadCallback (_pltState plt) -- | Convert the mutable state to a persistent one, setting it as the state of the provided token -- index. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. setTokenState :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> StateV1.MutableState -> ProtocolLevelTokensForPV pv -> m (ProtocolLevelTokensForPV pv) -setTokenState index mutableState pvPLTs = do - plts <- loadPLTs pvPLTs +setTokenState index mutableState pltsV0 = do + plts <- loadPLTs pltsV0 LFMBTree.update upd index (_pltTable plts) >>= \case Nothing -> error $ @@ -358,40 +384,46 @@ updateTokenState key maybeValue mutableState = -- | Get the configuration data of a token for a given 'TokenIndex'. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. getTokenConfiguration :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> ProtocolLevelTokensForPV pv -> m PLTConfiguration -getTokenConfiguration index pvPLTs = do - plt <- lookupPLT index pvPLTs +getTokenConfiguration index pltsV0 = do + plt <- lookupPLT index pltsV0 refLoad (_pltConfiguration plt) -- | Get the circulating supply of a token for a given 'TokenIndex'. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. getTokenCirculatingSupply :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> ProtocolLevelTokensForPV pv -> m TokenRawAmount -getTokenCirculatingSupply index pvPLTs = do - plt <- lookupPLT index pvPLTs +getTokenCirculatingSupply index pltsV0 = do + plt <- lookupPLT index pltsV0 return $ _pltCirculatingSupply plt -- | Set the circulating supply of a token for a given 'TokenIndex'. -- Returns the updated 'ProtocolLevelTokensForPV'. -- --- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokensForPV'. +-- PRECONDITION: The 'TokenIndex' MUST exist in the given 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. setTokenCirculatingSupply :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => TokenIndex -> TokenRawAmount -> ProtocolLevelTokensForPV pv -> m (ProtocolLevelTokensForPV pv) -setTokenCirculatingSupply index newSupply pvPLTs = do - plts <- loadPLTs pvPLTs +setTokenCirculatingSupply index newSupply pltsV0 = do + plts <- loadPLTs pltsV0 LFMBTree.update upd index (_pltTable plts) >>= \case Nothing -> error $ @@ -406,14 +438,16 @@ setTokenCirculatingSupply index newSupply pvPLTs = do -- initial supply will be 0. Returns the token index and the updated 'ProtocolLevelTokensForPV'. -- -- PRECONDITION: The 'TokenId' of the given configuration MUST NOT already exist in the --- 'ProtocolLevelTokensForPV'. +-- 'ProtocolLevelTokens'. +-- +-- This implementation is for the Haskell managed state version V0. createToken :: - (PVSupportsPLT pv, MonadBlobStore m) => + (MonadBlobStore m, PltStateVersionFor pv ~ 'PLTStateV0) => PLTConfiguration -> ProtocolLevelTokensForPV pv -> m (TokenIndex, ProtocolLevelTokensForPV pv) -createToken config pvPLTs = do - plts <- loadPLTs pvPLTs +createToken config pltsV0 = do + plts <- loadPLTs pltsV0 newConfigRef <- refMake config state <- liftIO StateV1.emptyPersistentState let plt = @@ -424,8 +458,8 @@ createToken config pvPLTs = do } (tokIndex, newTable) <- LFMBTree.append plt (_pltTable plts) let newMap = Map.insert (normalizeTokenId (_pltTokenId config)) tokIndex (_pltMap plts) - pvPLTs' <- storePLTs ProtocolLevelTokens{_pltTable = newTable, _pltMap = newMap} - return (tokIndex, pvPLTs') + pltsV0' <- storePLTs ProtocolLevelTokens{_pltTable = newTable, _pltMap = newMap} + return (tokIndex, pltsV0') -- | Migrate 'ProtocolLevelTokens' unchanged. migrateProtocolLevelTokens :: @@ -451,27 +485,45 @@ migrateProtocolLevelTokens ProtocolLevelTokens{..} = do _pltCirculatingSupply = _pltCirculatingSupply } --- | Migrate 'ProtocolLevelTokensForPV'. Where the old protocol version did not support PLTs, and --- the new protocol version does, this initializes the empty 'ProtocolLevelTokens'. Otherwise, --- the PLTs are unchanged in the migration. +-- | Migrate 'ProtocolLevelTokensForPV'. When the old protocol version does support PLTs ('SPLTStateNone'), and +-- the new protocol version does, this initializes the empty 'ProtocolLevelTokensForPV'. Migration of Haskell maintained +-- state to Rust maintaned state happens from 'SPLTStateV1' to 'SPLTStateV2'. migrateProtocolLevelTokensForPV :: forall t m. ( SupportMigration m t, + MonadProtocolVersion m, MonadProtocolVersion (t m) ) => StateMigrationParameters (MPV m) (MPV (t m)) -> ProtocolLevelTokensForPV (MPV m) -> t m (ProtocolLevelTokensForPV (MPV (t m))) -migrateProtocolLevelTokensForPV _ (ProtocolLevelTokensForPV CFalse) = do +migrateProtocolLevelTokensForPV _ ProtocolLevelTokensNone = do -- When migrating from a version where there are no protocol-level tokens, we use the -- empty protocol level tokens (if the new state supports LTS). emptyProtocolLevelTokensForPV -migrateProtocolLevelTokensForPV migration oldPLTsPV@(ProtocolLevelTokensForPV (CTrue _)) = - case sSupportsPLT (accountVersion @(AccountVersionFor (MPV (t m)))) of - STrue -> do - oldPLTs <- lift $ loadPLTs oldPLTsPV +migrateProtocolLevelTokensForPV migration oldPLTsV0@(ProtocolLevelTokensV0 oldPLTsRef) = + case pltStateVersion @(PltStateVersionFor (MPV (t m))) of + SPLTStateNone -> case migration of {} + SPLTStateV0 -> do + oldPLTs <- lift $ loadPLTs oldPLTsV0 newPLTs <- migrateProtocolLevelTokens oldPLTs storePLTs newPLTs - SFalse -> - -- There are no migrations that remove PLTs altogether. - case migration of {} + SPLTStateV1 -> do + -- Migrate from Haskell to Rust by reloading the PLT state from the blob store and into Rust and then + -- migrate the newly loaded state in the Rust block state implementation. + -- 1. Get blob reference + let oldBlobRefMaybe = getHBRRefIfBlobbed oldPLTsRef + let oldBlobRef = + maybe + (error "Haskell maintained PLT state to migrate from does not have blob reference set") + id + oldBlobRefMaybe + -- 2. Load it into Rust block state (supports P10) + (oldState :: ForeignPLTBlockStatePtr (MPV m)) <- lift $ loadDirect (coerce oldBlobRef) + -- 3. Migrate Rust block state from P10 to P11 + ProtocolLevelTokensV1 <$> RustBS.migrate oldState +migrateProtocolLevelTokensForPV migration (ProtocolLevelTokensV1 oldState) = + case pltStateVersion @(PltStateVersionFor (MPV (t m))) of + SPLTStateNone -> case migration of {} + SPLTStateV0 -> case migration of {} + SPLTStateV1 -> ProtocolLevelTokensV1 <$> RustBS.migrate oldState diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens/RustPLTBlockState.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens/RustPLTBlockState.hs new file mode 100644 index 0000000000..ab919fcdd4 --- /dev/null +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/ProtocolLevelTokens/RustPLTBlockState.hs @@ -0,0 +1,241 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Bindings to the Rust PLT block state implementation. +-- +-- Each foreign imported function must match the signature of functions found on the Rust side. +module Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState ( + RustPLTBlockState, + ForeignPLTBlockStatePtr, + wrapFFIPtr, + empty, + withPLTBlockState, + migrate, + ProtocolLevelTokensHash (..), +) where + +import Control.Monad.Trans (lift, liftIO) +import qualified Data.Serialize as S +import qualified Foreign as FFI + +import qualified Concordium.Crypto.SHA256 as SHA256 +import qualified Concordium.Types as Types +import qualified Concordium.Types.HashableTo as Hashable +import qualified Control.Monad as Monad +import qualified Data.FixedByteString as FixedByteString + +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore + +-- | Opaque type representing a Rust maintained PLT state. +-- The value is allocated in Rust and must be deallocated in Rust. +data RustPLTBlockState + +-- | Opaque pointer to a immutable PLT block state save-point managed by the rust library. +-- +-- Memory is deallocated using a finalizer. +newtype ForeignPLTBlockStatePtr (pv :: Types.ProtocolVersion) = ForeignPLTBlockStatePtr (FFI.ForeignPtr RustPLTBlockState) + +-- | Helper function to convert a raw pointer passed by the Rust library into a `PLTBlockState` object. +wrapFFIPtr :: FFI.Ptr RustPLTBlockState -> IO (ForeignPLTBlockStatePtr pv) +wrapFFIPtr blockStatePtr = ForeignPLTBlockStatePtr <$> FFI.newForeignPtr ffiFreePLTBlockState blockStatePtr + +-- | Deallocate a pointer to `PLTBlockState`. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall unsafe "&ffi_free_plt_block_state" + ffiFreePLTBlockState :: FFI.FinalizerPtr RustPLTBlockState + +-- | Get temporary access to the block state pointer. The pointer should not be +-- leaked from the computation. +-- +-- This ensures the finalizer is not called until the computation is over. +withPLTBlockState :: ForeignPLTBlockStatePtr pv -> (FFI.Ptr RustPLTBlockState -> IO a) -> IO a +withPLTBlockState (ForeignPLTBlockStatePtr foreignPtr) = FFI.withForeignPtr foreignPtr + +-- | Allocate new empty block state. +empty :: + forall m pv. + (BlobStore.MonadBlobStore m, Types.IsProtocolVersion pv) => + m (ForeignPLTBlockStatePtr pv) +empty = liftIO $ do + FFI.alloca $ \blockStateDestPtr -> do + status <- + ffiEmptyPLTBlockState + (sProtocolVersionToWord64 $ Types.protocolVersion @pv) + blockStateDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when creating a new block state" + blockState <- FFI.peek blockStateDestPtr + wrapFFIPtr blockState + +sProtocolVersionToWord64 :: + Types.SProtocolVersion pv -> + FFI.Word64 +sProtocolVersionToWord64 spv = Types.protocolVersionToWord64 $ Types.demoteProtocolVersion spv + +-- | Allocate new empty block state. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_empty_plt_block_state" + ffiEmptyPLTBlockState :: + -- | Protocol version of the block. + FFI.Word64 -> + -- | Destination pointer for the loaded block state. + FFI.Ptr (FFI.Ptr RustPLTBlockState) -> + -- | Status code + IO FFI.Word8 + +instance + (BlobStore.MonadBlobStore m, Types.IsProtocolVersion pv) => + BlobStore.BlobStorable m (ForeignPLTBlockStatePtr pv) + where + load = do + blobRef <- S.get + pure $! do + loadCallback <- fst <$> BlobStore.getCallbacks + liftIO $! do + FFI.alloca $ \blockStateDestPtr -> do + status <- + ffiLoadPLTBlockState + loadCallback + blobRef + (sProtocolVersionToWord64 $ Types.protocolVersion @pv) + blockStateDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when loading a block state" + blockState <- FFI.peek blockStateDestPtr + wrapFFIPtr blockState + storeUpdate pltBlockState = do + storeCallback <- snd <$> BlobStore.getCallbacks + blobRef <- liftIO $ FFI.alloca $ \blobRefDestPtr -> do + status <- withPLTBlockState pltBlockState $ ffiStorePLTBlockState storeCallback blobRefDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when storing a block state" + BlobStore.BlobRef @RustPLTBlockState <$> FFI.peek blobRefDestPtr + return (S.put blobRef, pltBlockState) + +-- | Load PLT block state from the given disk reference. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_load_plt_block_state" + ffiLoadPLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Reference in the blob store. + BlobStore.BlobRef RustPLTBlockState -> + -- | Protocol version of the block. + FFI.Word64 -> + -- | Destination pointer for the loaded block state. + FFI.Ptr (FFI.Ptr RustPLTBlockState) -> + -- | Status code + IO FFI.Word8 + +-- | Write out the block state using the provided callback, and return a `BlobRef`. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_store_plt_block_state" + ffiStorePLTBlockState :: + -- | The provided closure is called to write data to blob store. + FFI.StoreCallback -> + -- | Destination for the new reference in the blob store. + FFI.Ptr FFI.Word64 -> + -- | Pointer to the block state to write. + FFI.Ptr RustPLTBlockState -> + -- | Status code + IO FFI.Word8 + +instance (BlobStore.MonadBlobStore m) => BlobStore.Cacheable m (ForeignPLTBlockStatePtr pv) where + cache blockState = do + loadCallback <- fst <$> BlobStore.getCallbacks + status <- liftIO $! withPLTBlockState blockState (ffiCachePLTBlockState loadCallback) + Monad.unless (status == 0) $ error "Unexpected panic when caching a block state" + return blockState + +-- | Cache block state into memory. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_cache_plt_block_state" + ffiCachePLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Pointer to the block state to cache into memory. + FFI.Ptr RustPLTBlockState -> + IO FFI.Word8 + +-- | The hash of protocol-levels tokens state. +newtype ProtocolLevelTokensHash = ProtocolLevelTokensHash {theProtocolLevelTokensHash :: SHA256.Hash} + deriving newtype (Eq, Ord, Show, S.Serialize) + +instance + (BlobStore.MonadBlobStore m) => + Hashable.MHashableTo m ProtocolLevelTokensHash (ForeignPLTBlockStatePtr pv) + where + getHashM blockState = do + loadCallback <- fst <$> BlobStore.getCallbacks + ((), hash) <- + liftIO $ + withPLTBlockState blockState $ \blockStatePtr -> + FixedByteString.createWith $ \hashDestPtr -> do + status <- ffiHashPLTBlockState loadCallback blockStatePtr hashDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when hashing a block state" + return $ ProtocolLevelTokensHash (SHA256.Hash hash) + +-- | Compute the hash of the block state. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_hash_plt_block_state" + ffiHashPLTBlockState :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Pointer to the block state to hash. + FFI.Ptr RustPLTBlockState -> + -- | Pointer to write destination of the hash + FFI.Ptr FFI.Word8 -> + -- | Status code + IO FFI.Word8 + +-- | Run migration during a protocol update. +migrate :: + forall m t oldpv pv. + (BlobStore.SupportMigration m t, Types.IsProtocolVersion pv) => + -- | Current block state + (ForeignPLTBlockStatePtr oldpv) -> + -- | New migrated block state + t m (ForeignPLTBlockStatePtr pv) +migrate currentState = do + oldLoadCallback <- fst <$> lift BlobStore.getCallbacks + (newLoadCallback, newStoreCallback) <- BlobStore.getCallbacks + let newSProtocolVersion = Types.protocolVersion @pv + liftIO $ FFI.alloca $ \newStateDestPtr -> do + status <- + withPLTBlockState currentState $ + ffiMigratePLTBlockState + oldLoadCallback + newStoreCallback + newLoadCallback + (sProtocolVersionToWord64 newSProtocolVersion) + newStateDestPtr + Monad.unless (status == 0) $ error "Unexpected panic when migrating a block state" + newState <- FFI.peek newStateDestPtr + wrapFFIPtr newState + +-- | Migrate PLT block state from one blob store to another. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_migrate_plt_block_state" + ffiMigratePLTBlockState :: + -- | Called to read data from the blob store being migrated from. + FFI.LoadCallback -> + -- | Called to write data to the blob store being migrated to. + FFI.StoreCallback -> + -- | Called to read data from the blob store being migrated to. + FFI.LoadCallback -> + -- | Protocol version of the block being migrated to. + FFI.Word64 -> + -- | Pointer to the new block state. + FFI.Ptr (FFI.Ptr RustPLTBlockState) -> + -- | Block state to migrate from + FFI.Ptr RustPLTBlockState -> + -- | Status code + IO FFI.Word8 diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Updates.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Updates.hs index 42f6a216b0..51fe4acf47 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Updates.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/BlockState/Updates.hs @@ -34,9 +34,10 @@ import Concordium.Utils.Serialization.Put import Concordium.GlobalState.Parameters import Concordium.GlobalState.Persistent.BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.Parameters as PCP +import Concordium.GlobalState.Persistent.Migration import qualified Concordium.Types.AnonymityRevokers as ARS import qualified Concordium.Types.IdentityProviders as IPS -import Concordium.Types.Migration import qualified Concordium.Types.UpdateQueues as UQ -- | An update queue consists of pending future updates ordered by @@ -249,9 +250,43 @@ data PendingUpdates (cpv :: ChainParametersVersion) (auv :: AuthorizationsVersio -- | Finalization committee parameters (CPV2 onwards). pFinalizationCommitteeParametersQueue :: !(HashedBufferedRefO 'PTFinalizationCommitteeParameters cpv (UpdateQueue FinalizationCommitteeParameters)), -- | Validators score parameters (CPV3 onwards). - pValidatorScoreParametersQueue :: !(HashedBufferedRefO 'PTValidatorScoreParameters cpv (UpdateQueue ValidatorScoreParameters)) + pValidatorScoreParametersQueue :: !(HashedBufferedRefO 'PTValidatorScoreParameters cpv (UpdateQueue ValidatorScoreParameters)), + -- | Max lock duration (P11/AUV3 onwards). + pMaxLockDurationQueue :: !(Conditionally (SupportsTokenParameters auv) (HashedBufferedRef (UpdateQueue Duration))) } +-- | Migrate a conditionally-present update queue. +-- +-- * If the queue is not present in the new 'ChainParametersVersion', then this simply returns +-- 'NoParam'. +-- * If the queue is present in the new 'ChainParametersVersion' but not in the old one, this +-- creates a new empty queue. +-- * Otherwise, this creates a new queue, migrating all elements using the supplied migration +-- function. +migrateUpdateQueueRefO :: + forall paramType e1 e2 oldcpv newcpv t m. + ( SingI paramType, + SupportMigration m t, + IsChainParametersVersion newcpv, + Serialize e1, + Serialize e2, + MHashableTo (t m) H.Hash e2, + BlobStorable (t m) e2 + ) => + -- | Function for migrating queue elements. + ((IsSupported paramType oldcpv ~ 'True, IsSupported paramType newcpv ~ 'True) => e1 -> e2) -> + -- | The queue before migration. + HashedBufferedRefO paramType oldcpv (UpdateQueue e1) -> + t m (HashedBufferedRefO paramType newcpv (UpdateQueue e2)) +migrateUpdateQueueRefO migrateValue = case sIsSupported (sing @paramType) (chainParametersVersion @newcpv) of + SFalse -> \_ -> return NoParam + STrue -> \case + NoParam -> do + (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue + return (SomeParam hbr) + SomeParam hbr -> do + SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue migrateValue) hbr + -- | See documentation of @migratePersistentBlockState@. migratePendingUpdates :: forall oldpv pv t m. @@ -262,7 +297,7 @@ migratePendingUpdates :: StateMigrationParameters oldpv pv -> PendingUpdates (ChainParametersVersionFor oldpv) (AuthorizationsVersionFor oldpv) -> t m (PendingUpdates (ChainParametersVersionFor pv) (AuthorizationsVersionFor pv)) -migratePendingUpdates migration PendingUpdates{..} = withCPVConstraints (chainParametersVersion @(ChainParametersVersionFor oldpv)) $ withCPVConstraints (chainParametersVersion @(ChainParametersVersionFor pv)) $ do +migratePendingUpdates migration PendingUpdates{..} = withCPVConstraints oldCPV $ withCPVConstraints newCPV $ do newRootKeys <- migrateHashedBufferedRef (migrateUpdateQueue id) pRootKeysUpdateQueue newLevel1Keys <- migrateHashedBufferedRef (migrateUpdateQueue id) pLevel1KeysUpdateQueue newLevel2Keys <- @@ -299,190 +334,45 @@ migratePendingUpdates migration PendingUpdates{..} = withCPVConstraints (chainPa pPoolParametersQueue newAddAnonymityRevokers <- migrateHashedBufferedRef (migrateUpdateQueue id) pAddAnonymityRevokerQueue newAddIdentityProviders <- migrateHashedBufferedRef (migrateUpdateQueue id) pAddIdentityProviderQueue - newElectionDifficulty <- case migration of - StateMigrationParametersTrivial -> case pElectionDifficultyQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pElectionDifficultyQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP2P3 -> case pElectionDifficultyQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP3ToP4{} -> case pElectionDifficultyQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP4ToP5{} -> case pElectionDifficultyQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP5ToP6{} -> case pElectionDifficultyQueue of - SomeParam _ -> return NoParam - StateMigrationParametersP6ToP7{} -> case pElectionDifficultyQueue of - NoParam -> return NoParam - StateMigrationParametersP7ToP8{} -> case pElectionDifficultyQueue of - NoParam -> return NoParam - StateMigrationParametersP8ToP9{} -> case pElectionDifficultyQueue of - NoParam -> return NoParam - StateMigrationParametersP9ToP10{} -> case pElectionDifficultyQueue of - NoParam -> return NoParam - newTimeParameters <- case migration of - StateMigrationParametersTrivial -> case pTimeParametersQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pTimeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pTimeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP4ToP5{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP5ToP6{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP6ToP7{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pTimeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newCooldownParameters <- case migration of - StateMigrationParametersTrivial -> case pCooldownParametersQueue of - NoParam -> return NoParam - SomeParam hbr -> - SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pCooldownParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pCooldownParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP4ToP5{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP5ToP6{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP6ToP7{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pCooldownParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newTimeoutParameters <- case migration of - StateMigrationParametersTrivial -> case pTimeoutParametersQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pTimeoutParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pTimeoutParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> case pTimeoutParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP4ToP5{} -> case pTimeoutParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP5ToP6{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP6ToP7{} -> case pTimeoutParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pTimeoutParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pTimeoutParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pTimeoutParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newMinBlockTimeQueue <- case migration of - StateMigrationParametersTrivial -> case pMinBlockTimeQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pMinBlockTimeQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pMinBlockTimeQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> case pMinBlockTimeQueue of - NoParam -> return NoParam - StateMigrationParametersP4ToP5{} -> case pMinBlockTimeQueue of - NoParam -> return NoParam - StateMigrationParametersP5ToP6{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP6ToP7{} -> case pMinBlockTimeQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pMinBlockTimeQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pMinBlockTimeQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pMinBlockTimeQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newBlockEnergyLimitQueue <- case migration of - StateMigrationParametersTrivial -> case pBlockEnergyLimitQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pBlockEnergyLimitQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pBlockEnergyLimitQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> case pBlockEnergyLimitQueue of - NoParam -> return NoParam - StateMigrationParametersP4ToP5{} -> case pBlockEnergyLimitQueue of - NoParam -> return NoParam - StateMigrationParametersP5ToP6{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP6ToP7{} -> case pBlockEnergyLimitQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pBlockEnergyLimitQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pBlockEnergyLimitQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pBlockEnergyLimitQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newFinalizationCommitteeParametersQueue <- case migration of - StateMigrationParametersTrivial -> case pFinalizationCommitteeParametersQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pFinalizationCommitteeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pFinalizationCommitteeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> case pFinalizationCommitteeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP4ToP5{} -> case pFinalizationCommitteeParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP5ToP6{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP6ToP7{} -> case pFinalizationCommitteeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP7ToP8{} -> case pFinalizationCommitteeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP8ToP9{} -> case pFinalizationCommitteeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pFinalizationCommitteeParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - newValidatorScoreParametersQueue <- case migration of - StateMigrationParametersTrivial -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP1P2 -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP2P3 -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP3ToP4{} -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP4ToP5{} -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP5ToP6{} -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP6ToP7{} -> case pValidatorScoreParametersQueue of - NoParam -> return NoParam - StateMigrationParametersP7ToP8{} -> do - (!hbr, _) <- refFlush =<< refMake emptyUpdateQueue - return (SomeParam hbr) - StateMigrationParametersP8ToP9{} -> case pValidatorScoreParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr - StateMigrationParametersP9ToP10{} -> case pValidatorScoreParametersQueue of - SomeParam hbr -> SomeParam <$> migrateHashedBufferedRef (migrateUpdateQueue id) hbr + -- Election difficulty is present in P1-P5 and was removed from P6. + newElectionDifficulty <- migrateUpdateQueueRefO id pElectionDifficultyQueue + -- Time parameters were introduced in P4. + newTimeParameters <- migrateUpdateQueueRefO id pTimeParametersQueue + -- Cooldown parameters were introduced in P4. + -- Note, here the CooldownParametersVersion is CooldownParametersVersion1 whenever we + -- actually have a queue. However, the queue type is parametrised so we have to prove this + -- fact by showing other cases are not possible. + newCooldownParameters <- + migrateUpdateQueueRefO + ( case sCooldownParametersVersionFor newCPV of + SCooldownParametersVersion0 -> case newCPV of {} + SCooldownParametersVersion1 -> case sCooldownParametersVersionFor oldCPV of + SCooldownParametersVersion0 -> case oldCPV of {} + SCooldownParametersVersion1 -> id + ) + pCooldownParametersQueue + -- Timeout parameters were introduced in P6. + newTimeoutParameters <- migrateUpdateQueueRefO id pTimeoutParametersQueue + -- Min block time was introduced in P6. + newMinBlockTimeQueue <- migrateUpdateQueueRefO id pMinBlockTimeQueue + -- Block energy limit was introduced in P6. + newBlockEnergyLimitQueue <- migrateUpdateQueueRefO id pBlockEnergyLimitQueue + -- Finalization committee parameters were introduced in P6. + newFinalizationCommitteeParametersQueue <- + migrateUpdateQueueRefO id pFinalizationCommitteeParametersQueue + -- Validator score parameters were introduced in P8. + newValidatorScoreParametersQueue <- migrateUpdateQueueRefO id pValidatorScoreParametersQueue + -- Max lock duration was introduced in P11/AUV3. + let oldSupportsTokenParameters = sSupportsTokenParameters (sAuthorizationsVersionFor (protocolVersion @oldpv)) + newSupportsTokenParameters = sSupportsTokenParameters (sAuthorizationsVersionFor (protocolVersion @pv)) + newMaxLockDurationQueue <- case (oldSupportsTokenParameters, newSupportsTokenParameters) of + (STrue, STrue) -> case pMaxLockDurationQueue of + CTrue queueRef -> CTrue <$> migrateHashedBufferedRef (migrateUpdateQueue id) queueRef + (STrue, SFalse) -> return CFalse + (SFalse, STrue) -> do + (!queueRef, _) <- refFlush =<< refMake emptyUpdateQueue + return (CTrue queueRef) + (SFalse, SFalse) -> return CFalse return $! PendingUpdates { pRootKeysUpdateQueue = newRootKeys, @@ -505,8 +395,12 @@ migratePendingUpdates migration PendingUpdates{..} = withCPVConstraints (chainPa pMinBlockTimeQueue = newMinBlockTimeQueue, pBlockEnergyLimitQueue = newBlockEnergyLimitQueue, pFinalizationCommitteeParametersQueue = newFinalizationCommitteeParametersQueue, - pValidatorScoreParametersQueue = newValidatorScoreParametersQueue + pValidatorScoreParametersQueue = newValidatorScoreParametersQueue, + pMaxLockDurationQueue = newMaxLockDurationQueue } + where + oldCPV = chainParametersVersion @(ChainParametersVersionFor oldpv) + newCPV = chainParametersVersion @(ChainParametersVersionFor pv) instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => @@ -534,6 +428,7 @@ instance hBlockEnergyLimitQueue <- hashWhenSupported pBlockEnergyLimitQueue hFinalizationCommitteeParametersQueue <- hashWhenSupported pFinalizationCommitteeParametersQueue hValidatorScoreParametersQueue <- hashWhenSupported pValidatorScoreParametersQueue + hMaxLockDurationQueue <- hashWhenConditionallySupported pMaxLockDurationQueue return $! H.hash $ hRootKeysUpdateQueue @@ -557,9 +452,13 @@ instance <> hBlockEnergyLimitQueue <> hFinalizationCommitteeParametersQueue <> hValidatorScoreParametersQueue + <> hMaxLockDurationQueue where hashWhenSupported :: (MHashableTo m H.Hash a) => OParam pt cpv a -> m BS.ByteString hashWhenSupported = maybeWhenSupported (return mempty) (fmap H.hashToByteString . getHashM) + hashWhenConditionallySupported :: (MHashableTo m H.Hash a) => Conditionally b a -> m BS.ByteString + hashWhenConditionallySupported CFalse = return mempty + hashWhenConditionallySupported (CTrue value) = H.hashToByteString <$> getHashM value instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => @@ -587,6 +486,11 @@ instance (putBlockEnergyLimitQueue, newBlockEnergyLimitQueue) <- storeUpdate pBlockEnergyLimitQueue (putFinalizationCommitteeParametersQueue, newFinalizationCommitteeParametersQueue) <- storeUpdate pFinalizationCommitteeParametersQueue (putValidatorScoreParametersQueue, newValidatorScoreParametersQueue) <- storeUpdate pValidatorScoreParametersQueue + (putMaxLockDurationQueue, newMaxLockDurationQueue) <- case pMaxLockDurationQueue of + CFalse -> return (return (), CFalse) + CTrue queue -> do + (putQueue, newQueue) <- storeUpdate queue + return (putQueue, CTrue newQueue) let newPU = PendingUpdates { pRootKeysUpdateQueue = rkQ, @@ -609,7 +513,8 @@ instance pMinBlockTimeQueue = newMinBlockTimeQueue, pBlockEnergyLimitQueue = newBlockEnergyLimitQueue, pFinalizationCommitteeParametersQueue = newFinalizationCommitteeParametersQueue, - pValidatorScoreParametersQueue = newValidatorScoreParametersQueue + pValidatorScoreParametersQueue = newValidatorScoreParametersQueue, + pMaxLockDurationQueue = newMaxLockDurationQueue } let putPU = pRKQ @@ -633,6 +538,7 @@ instance >> putBlockEnergyLimitQueue >> putFinalizationCommitteeParametersQueue >> putValidatorScoreParametersQueue + >> putMaxLockDurationQueue return (putPU, newPU) load = withCPVConstraints (chainParametersVersion @cpv) $ do mRKQ <- label "Root keys update queue" load @@ -656,6 +562,7 @@ instance mBlockEnergyLimitQueue <- label "Block energy limit update queue" load mFinalizationCommitteeParametersQueue <- label "Finalization committee parameters update queue" load mValidatorScoreParametersQueue <- label "Validator score parameters update queue" load + mMaxLockDurationQueue <- conditionallyA (sSupportsTokenParameters (authorizationsVersion @auv)) $ label "Max lock duration update queue" load return $! do pRootKeysUpdateQueue <- mRKQ pLevel1KeysUpdateQueue <- mL1KQ @@ -678,6 +585,7 @@ instance pBlockEnergyLimitQueue <- mBlockEnergyLimitQueue pFinalizationCommitteeParametersQueue <- mFinalizationCommitteeParametersQueue pValidatorScoreParametersQueue <- mValidatorScoreParametersQueue + pMaxLockDurationQueue <- sequenceA mMaxLockDurationQueue return PendingUpdates{..} instance @@ -708,15 +616,39 @@ instance <*> cache pBlockEnergyLimitQueue <*> cache pFinalizationCommitteeParametersQueue <*> cache pValidatorScoreParametersQueue + <*> traverse cache pMaxLockDurationQueue where cpv = chainParametersVersion @cpv -- | Initial pending updates with empty queues. emptyPendingUpdates :: forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv) => + (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => m (PendingUpdates cpv auv) -emptyPendingUpdates = PendingUpdates <$> e <*> e <*> e <*> e <*> whenSupportedA e <*> e <*> e <*> e <*> e <*> e <*> e <*> e <*> e <*> e <*> whenSupportedA e <*> whenSupportedA e <*> whenSupportedA e <*> whenSupportedA e <*> whenSupportedA e <*> whenSupportedA e <*> whenSupportedA e +emptyPendingUpdates = + PendingUpdates + <$> e + <*> e + <*> e + <*> e + <*> whenSupportedA e + <*> e + <*> e + <*> e + <*> e + <*> e + <*> e + <*> e + <*> e + <*> e + <*> whenSupportedA e + <*> whenSupportedA e + <*> whenSupportedA e + <*> whenSupportedA e + <*> whenSupportedA e + <*> whenSupportedA e + <*> whenSupportedA e + <*> conditionallyA (sSupportsTokenParameters (authorizationsVersion @auv)) e where e :: m (HashedBufferedRef (UpdateQueue a)) e = makeHashedBufferedRef emptyUpdateQueue @@ -749,6 +681,7 @@ makePersistentPendingUpdates UQ.PendingUpdates{..} = withCPVConstraints (chainPa pBlockEnergyLimitQueue <- mapM (refMake <=< makePersistentUpdateQueue) _pBlockEnergyLimitQueue pFinalizationCommitteeParametersQueue <- mapM (refMake <=< makePersistentUpdateQueue) _pFinalizationCommitteeParametersQueue pValidatorScoreParametersQueue <- mapM (refMake <=< makePersistentUpdateQueue) _pValidatorScoreParametersQueue + pMaxLockDurationQueue <- traverse (refMake <=< makePersistentUpdateQueue) _pMaxLockDurationQueue return PendingUpdates{..} -- | Convert a persistent 'PendingUpdates' to an in-memory 'UQ.PendingUpdates'. @@ -779,20 +712,27 @@ makeBasicPendingUpdates PendingUpdates{..} = withCPVConstraints (chainParameters _pBlockEnergyLimitQueue <- mapM (makeBasicUpdateQueue <=< refLoad) pBlockEnergyLimitQueue _pFinalizationCommitteeParametersQueue <- mapM (makeBasicUpdateQueue <=< refLoad) pFinalizationCommitteeParametersQueue _pValidatorScoreParametersQueue <- mapM (makeBasicUpdateQueue <=< refLoad) pValidatorScoreParametersQueue + _pMaxLockDurationQueue <- traverse (makeBasicUpdateQueue <=< refLoad) pMaxLockDurationQueue return UQ.PendingUpdates{..} +-- | Update value indexed by protocol version. +type UpdateValueFor (pv :: ProtocolVersion) = UpdateValue (ChainParametersVersionFor pv) (AuthorizationsVersionFor pv) + +-- | Pending updates indexed by protocol version. +type PendingUpdatesFor (pv :: ProtocolVersion) = PendingUpdates (ChainParametersVersionFor pv) (AuthorizationsVersionFor pv) + -- | Current state of updatable parameters and update queues. -data Updates' (cpv :: ChainParametersVersion) (auv :: AuthorizationsVersion) = Updates +data Updates (pv :: ProtocolVersion) = Updates { -- | Current update authorizations. - currentKeyCollection :: !(HashedBufferedRef (StoreSerialized (UpdateKeysCollection auv))), + currentKeyCollection :: !(HashedBufferedRef (StoreSerialized (UpdateKeysCollection (AuthorizationsVersionFor pv)))), -- | Current protocol update. currentProtocolUpdate :: !(Nullable (HashedBufferedRef (StoreSerialized ProtocolUpdate))), - -- | Current chain parameters. - currentParameters :: !(HashedBufferedRef (StoreSerialized (ChainParameters' cpv))), + -- | Current node-owned persistent chain parameters. + currentParameters :: !(HashedBufferedRef (PCP.PersistentChainParameters pv)), -- | Pending updates. - pendingUpdates :: !(PendingUpdates cpv auv), + pendingUpdates :: !(PendingUpdatesFor pv), -- | Sequence number for updates to the protocol level tokens (PLT). - pltUpdateSequenceNumber :: !(Conditionally (SupportsCreatePLT auv) UpdateSequenceNumber) + pltUpdateSequenceNumber :: !(Conditionally (SupportsCreatePLT (AuthorizationsVersionFor pv)) UpdateSequenceNumber) } -- | See documentation of @migratePersistentBlockState@. @@ -818,7 +758,7 @@ migrateUpdates migration Updates{..} = do migrateHashedBufferedRef (return . StoreSerialized . migrateKeysCollection . unStoreSerialized) currentKeyCollection - newParameters <- migrateHashedBufferedRef (return . StoreSerialized . migrateChainParameters migration . unStoreSerialized) currentParameters + newParameters <- migrateHashedBufferedRef (migrateChainParameters migration) currentParameters let newPltUpdateSequenceNumber = case sSupportsCreatePLT (sAuthorizationsVersionFor (protocolVersion @oldpv)) of STrue -> case sSupportsCreatePLT (sAuthorizationsVersionFor (protocolVersion @pv)) of @@ -848,9 +788,7 @@ migrateUpdates migration Updates{..} = do pltUpdateSequenceNumber = newPltUpdateSequenceNumber } -type Updates (pv :: ProtocolVersion) = Updates' (ChainParametersVersionFor pv) (AuthorizationsVersionFor pv) - -instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => MHashableTo m H.Hash (Updates' cpv auv) where +instance (MonadBlobStore m, IsProtocolVersion pv) => MHashableTo m H.Hash (Updates pv) where getHashM Updates{..} = do hCA <- getHashM currentKeyCollection mHCPU <- mapM getHashM currentProtocolUpdate @@ -872,8 +810,8 @@ instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersio put usn instance - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BlobStorable m (Updates' cpv auv) + (MonadBlobStore m, IsProtocolVersion pv) => + BlobStorable m (Updates pv) where storeUpdate Updates{..} = do (pKC, kC) <- storeUpdate currentKeyCollection @@ -895,7 +833,7 @@ instance mCPU <- label "Current protocol update" load mCP <- label "Current parameters" load mPU <- label "Pending updates" load - pltUpdateSequenceNumber <- conditionallyA (sSupportsCreatePLT (sing @auv)) $ label "PLT sequence number" get + pltUpdateSequenceNumber <- conditionallyA (sSupportsCreatePLT (sing @(AuthorizationsVersionFor pv))) $ label "PLT sequence number" get return $! do currentKeyCollection <- mKC currentProtocolUpdate <- mCPU @@ -903,7 +841,7 @@ instance pendingUpdates <- mPU return Updates{..} -instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => Cacheable m (Updates' cpv auv) where +instance (MonadBlobStore m, IsProtocolVersion pv) => Cacheable m (Updates pv) where cache Updates{..} = Updates <$> cache currentKeyCollection @@ -915,41 +853,41 @@ instance (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersio -- | An initial 'Updates' with the given initial 'Authorizations' -- and 'ChainParameters'. initialUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - UpdateKeysCollection auv -> - ChainParameters' cpv -> - m (Updates' cpv auv) + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + UpdateKeysCollection (AuthorizationsVersionFor pv) -> + ChainParameters pv -> + m (Updates pv) initialUpdates initialKeyCollection chainParams = do currentKeyCollection <- makeHashedBufferedRef (StoreSerialized initialKeyCollection) let currentProtocolUpdate = Null - currentParameters <- makeHashedBufferedRef (StoreSerialized chainParams) + currentParameters <- makeHashedBufferedRef =<< PCP.makePersistentChainParameters chainParams pendingUpdates <- emptyPendingUpdates - let pltUpdateSequenceNumber = conditionally (sSupportsCreatePLT (authorizationsVersion @auv)) minUpdateSequenceNumber + let pltUpdateSequenceNumber = conditionally (sSupportsCreatePLT (sAuthorizationsVersionFor (protocolVersion @pv))) minUpdateSequenceNumber return Updates{..} -- | Make a persistent 'Updates' from an in-memory one. makePersistentUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - UQ.Updates' cpv auv -> - m (Updates' cpv auv) + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + UQ.Updates pv -> + m (Updates pv) makePersistentUpdates UQ.Updates{..} = do currentKeyCollection <- refMake (StoreSerialized (_unhashed _currentKeyCollection)) currentProtocolUpdate <- case _currentProtocolUpdate of Nothing -> return Null Just pu -> Some <$> refMake (StoreSerialized pu) - currentParameters <- refMake (StoreSerialized _currentParameters) + currentParameters <- refMake =<< PCP.makePersistentChainParameters _currentParameters pendingUpdates <- makePersistentPendingUpdates _pendingUpdates let pltUpdateSequenceNumber = _pltUpdateSequenceNumber return Updates{..} -- | Convert a persistent 'Updates' to an in-memory 'UQ.Updates'. makeBasicUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - (Updates' cpv auv) -> - m (UQ.Updates' cpv auv) + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + Updates pv -> + m (UQ.Updates pv) makeBasicUpdates Updates{..} = do hKC <- getHashM currentKeyCollection kc <- unStoreSerialized <$> refLoad currentKeyCollection @@ -957,11 +895,29 @@ makeBasicUpdates Updates{..} = do _currentProtocolUpdate <- case currentProtocolUpdate of Null -> return Nothing Some pu -> Just . unStoreSerialized <$> refLoad pu - _currentParameters <- unStoreSerialized <$> refLoad currentParameters + _currentParameters <- PCP.persistentChainParametersToChainParametersM =<< refLoad currentParameters _pendingUpdates <- makeBasicPendingUpdates pendingUpdates let _pltUpdateSequenceNumber = pltUpdateSequenceNumber return UQ.Updates{..} +-- | Load the public/wire view of current chain parameters from the persistent node representation. +loadChainParametersRef :: + (MonadBlobStore m, IsProtocolVersion pv) => + HashedBufferedRef (PCP.PersistentChainParameters pv) -> + m (ChainParameters pv) +loadChainParametersRef currentParameters = + PCP.persistentChainParametersToChainParameters <$> refLoad currentParameters + +-- | Store a new chain-parameter value while preserving node-internal external state. +makeUpdatedChainParametersRef :: + (MonadBlobStore m, IsProtocolVersion pv) => + HashedBufferedRef (PCP.PersistentChainParameters pv) -> + ChainParameters pv -> + m (HashedBufferedRef (PCP.PersistentChainParameters pv)) +makeUpdatedChainParametersRef currentParameters newChainParameters = do + persistentParameters <- refLoad currentParameters + refMake $ PCP.updateChainParameters newChainParameters persistentParameters + -- | Process the update queue to determine the new value of a parameter (or the authorizations). -- This splits the queue at the given timestamp. The last value up to and including the timestamp -- is the new value, if any -- otherwise the current value is retained. The queue is updated to @@ -988,11 +944,11 @@ processValueUpdates t uq noUpdate doUpdate = case ql of -- | Process root keys updates. processRootKeysUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processRootKeysUpdates t bu = do u@Updates{..} <- refLoad bu rootKeysQueue <- refLoad (pRootKeysUpdateQueue pendingUpdates) @@ -1010,11 +966,11 @@ processRootKeysUpdates t bu = do -- | Process level 1 keys updates. processLevel1KeysUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processLevel1KeysUpdates t bu = do u@Updates{..} <- refLoad bu level1KeysQueue <- refLoad (pLevel1KeysUpdateQueue pendingUpdates) @@ -1032,11 +988,11 @@ processLevel1KeysUpdates t bu = do -- | Process level 2 keys updates. processLevel2KeysUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processLevel2KeysUpdates t bu = do u@Updates{..} <- refLoad bu level2KeysQueue <- refLoad (pLevel2KeysUpdateQueue pendingUpdates) @@ -1054,10 +1010,10 @@ processLevel2KeysUpdates t bu = do -- | Process election difficulty updates. processElectionDifficultyUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processElectionDifficultyUpdates t bu = do u@Updates{..} <- refLoad bu case pElectionDifficultyQueue pendingUpdates of @@ -1067,11 +1023,11 @@ processElectionDifficultyUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newEDp newQ m -> (UVElectionDifficulty <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newED <- refLoad newEDp let newConsensusParameters = case oldCP ^. cpConsensusParameters of ConsensusParametersV0{} -> ConsensusParametersV0 newED - newParameters <- refMake $ StoreSerialized $ oldCP & (cpConsensusParameters .~ newConsensusParameters) + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & (cpConsensusParameters .~ newConsensusParameters) refMake u { currentParameters = newParameters, @@ -1080,19 +1036,19 @@ processElectionDifficultyUpdates t bu = do -- | Process Euro:energy rate updates. processEuroPerEnergyUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processEuroPerEnergyUpdates t bu = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pEuroPerEnergyQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newEPEp newQ m -> (UVEuroPerEnergy <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newEPE <- refLoad newEPEp - newParameters <- refMake $ StoreSerialized $ oldCP & euroPerEnergy .~ newEPE + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & euroPerEnergy .~ newEPE refMake u { currentParameters = newParameters, @@ -1101,19 +1057,19 @@ processEuroPerEnergyUpdates t bu = do -- | Process microGTU:Euro rate updates. processMicroGTUPerEuroUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processMicroGTUPerEuroUpdates t bu = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pMicroGTUPerEuroQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newMGTUPEp newQ m -> (UVMicroGTUPerEuro <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newMGTUPE <- refLoad newMGTUPEp - newParameters <- refMake $ StoreSerialized $ oldCP & microGTUPerEuro .~ newMGTUPE + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & microGTUPerEuro .~ newMGTUPE refMake u { currentParameters = newParameters, @@ -1121,19 +1077,19 @@ processMicroGTUPerEuroUpdates t bu = do } processFoundationAccountUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processFoundationAccountUpdates t bu = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pFoundationAccountQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVFoundationAccount <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & cpFoundationAccount .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & cpFoundationAccount .~ newParam refMake u { currentParameters = newParameters, @@ -1141,20 +1097,20 @@ processFoundationAccountUpdates t bu = do } processMintDistributionUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) -processMintDistributionUpdates t bu = withIsMintDistributionVersionFor (chainParametersVersion @cpv) $ do + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) +processMintDistributionUpdates t bu = withIsMintDistributionVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) $ do u@Updates{..} <- refLoad bu oldQ <- refLoad (pMintDistributionQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVMintDistribution <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & rpMintDistribution .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & rpMintDistribution .~ newParam refMake u { currentParameters = newParameters, @@ -1162,19 +1118,19 @@ processMintDistributionUpdates t bu = withIsMintDistributionVersionFor (chainPar } processTransactionFeeDistributionUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processTransactionFeeDistributionUpdates t bu = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pTransactionFeeDistributionQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVTransactionFeeDistribution <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & rpTransactionFeeDistribution .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & rpTransactionFeeDistribution .~ newParam refMake u { currentParameters = newParameters, @@ -1182,20 +1138,20 @@ processTransactionFeeDistributionUpdates t bu = do } processGASRewardsUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) -processGASRewardsUpdates t bu = withIsGASRewardsVersionFor (chainParametersVersion @cpv) $ do + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) +processGASRewardsUpdates t bu = withIsGASRewardsVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) $ do u@Updates{..} <- refLoad bu oldQ <- refLoad (pGASRewardsQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVGASRewards <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & rpGASRewards .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & rpGASRewards .~ newParam refMake u { currentParameters = newParameters, @@ -1203,20 +1159,20 @@ processGASRewardsUpdates t bu = withIsGASRewardsVersionFor (chainParametersVersi } processPoolParamatersUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) -processPoolParamatersUpdates t bu = withIsPoolParametersVersionFor (chainParametersVersion @cpv) $ do + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) +processPoolParamatersUpdates t bu = withIsPoolParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) $ do u@Updates{..} <- refLoad bu oldQ <- refLoad (pPoolParametersQueue pendingUpdates) processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVPoolParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & cpPoolParameters .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & cpPoolParameters .~ newParam refMake u { currentParameters = newParameters, @@ -1225,23 +1181,23 @@ processPoolParamatersUpdates t bu = withIsPoolParametersVersionFor (chainParamet -- | Process cooldown parameters updates. processCooldownParametersUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processCooldownParametersUpdates t bu = do u@Updates{..} <- refLoad bu case pCooldownParametersQueue pendingUpdates of NoParam -> return (Map.empty, bu) - SomeParam qref -> withIsCooldownParametersVersionFor (chainParametersVersion @cpv) $ do + SomeParam qref -> withIsCooldownParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) $ do oldQ <- refLoad qref processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVCooldownParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & cpCooldownParameters .~ newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & cpCooldownParameters .~ newParam refMake u { currentParameters = newParameters, @@ -1250,10 +1206,10 @@ processCooldownParametersUpdates t bu = do -- | Process time parameters updates. processTimeParametersUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processTimeParametersUpdates t bu = do u@Updates{..} <- refLoad bu case pTimeParametersQueue pendingUpdates of @@ -1263,9 +1219,9 @@ processTimeParametersUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newParamPtr newQ m -> (UVTimeParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newParam <- refLoad newParamPtr - newParameters <- refMake $ StoreSerialized $ oldCP & cpTimeParameters .~ SomeParam newParam + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & cpTimeParameters .~ SomeParam newParam refMake u { currentParameters = newParameters, @@ -1277,11 +1233,11 @@ processTimeParametersUpdates t bu = do -- update them (if an update was enqueued and its time is now) -- and update the 'pendingUpdates' accordingly. processTimeoutParametersUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processTimeoutParametersUpdates t bu = do u@Updates{..} <- refLoad bu case pTimeoutParametersQueue pendingUpdates of @@ -1291,11 +1247,11 @@ processTimeoutParametersUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newTOp newQ m -> (UVTimeoutParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newTimeoutParameters <- refLoad newTOp let newConsensusParameters = case oldCP ^. cpConsensusParameters of cp@ConsensusParametersV1{} -> cp & cpTimeoutParameters .~ newTimeoutParameters - newParameters <- refMake $ StoreSerialized $ oldCP & (cpConsensusParameters .~ newConsensusParameters) + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & (cpConsensusParameters .~ newConsensusParameters) refMake u { currentParameters = newParameters, @@ -1307,11 +1263,11 @@ processTimeoutParametersUpdates t bu = do -- update it (if an update was enqueued and its time is now) -- and update the 'pendingUpdates' accordingly. processMinBlockTimeUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processMinBlockTimeUpdates t bu = do u@Updates{..} <- refLoad bu case pMinBlockTimeQueue pendingUpdates of @@ -1321,11 +1277,11 @@ processMinBlockTimeUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newMBTp newQ m -> (UVMinBlockTime <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newMinBlockTime <- refLoad newMBTp let newConsensusParameters = case oldCP ^. cpConsensusParameters of cp@ConsensusParametersV1{} -> cp & cpMinBlockTime .~ newMinBlockTime - newParameters <- refMake $ StoreSerialized $ oldCP & (cpConsensusParameters .~ newConsensusParameters) + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & (cpConsensusParameters .~ newConsensusParameters) refMake u { currentParameters = newParameters, @@ -1337,11 +1293,11 @@ processMinBlockTimeUpdates t bu = do -- update it (if an update was enqueued and its time is now) -- and update the 'pendingUpdates' accordingly. processBlockEnergyLimitUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processBlockEnergyLimitUpdates t bu = do u@Updates{..} <- refLoad bu case pBlockEnergyLimitQueue pendingUpdates of @@ -1351,11 +1307,11 @@ processBlockEnergyLimitUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newBELp newQ m -> (UVBlockEnergyLimit <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newBlockEnergyLimit <- refLoad newBELp let newConsensusParameters = case oldCP ^. cpConsensusParameters of cp@ConsensusParametersV1{} -> cp & cpBlockEnergyLimit .~ newBlockEnergyLimit - newParameters <- refMake $ StoreSerialized $ oldCP & (cpConsensusParameters .~ newConsensusParameters) + newParameters <- makeUpdatedChainParametersRef currentParameters $ oldCP & (cpConsensusParameters .~ newConsensusParameters) refMake u { currentParameters = newParameters, @@ -1367,11 +1323,11 @@ processBlockEnergyLimitUpdates t bu = do -- update them (if an update was enqueued and its time is now) -- and update the 'pendingUpdates' accordingly. processFinalizationCommitteeParametersUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processFinalizationCommitteeParametersUpdates t bu = do u@Updates{..} <- refLoad bu case pFinalizationCommitteeParametersQueue pendingUpdates of @@ -1381,13 +1337,12 @@ processFinalizationCommitteeParametersUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newBELp newQ m -> (UVFinalizationCommitteeParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newFinalizationCommitteeParameters <- refLoad newBELp newParameters <- - refMake $ - StoreSerialized $ - oldCP - & (cpFinalizationCommitteeParameters . supportedOParam .~ newFinalizationCommitteeParameters) + makeUpdatedChainParametersRef currentParameters $ + oldCP + & (cpFinalizationCommitteeParameters . supportedOParam .~ newFinalizationCommitteeParameters) refMake u { currentParameters = newParameters, @@ -1399,11 +1354,11 @@ processFinalizationCommitteeParametersUpdates t bu = do -- update them (if an update was enqueued and its time is now) -- and update the 'pendingUpdates' accordingly. processValidationScoreParametersUpdates :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processValidationScoreParametersUpdates t bu = do u@Updates{..} <- refLoad bu case pValidatorScoreParametersQueue pendingUpdates of @@ -1413,27 +1368,56 @@ processValidationScoreParametersUpdates t bu = do processValueUpdates t oldQ (return (Map.empty, bu)) $ \newBELp newQ m -> (UVValidatorScoreParameters <$> m,) <$> do newpQ <- refMake newQ - StoreSerialized oldCP <- refLoad currentParameters + oldCP <- loadChainParametersRef currentParameters StoreSerialized newValidatorScoreParameters <- refLoad newBELp newParameters <- - refMake $ - StoreSerialized $ - oldCP - & (cpValidatorScoreParameters . supportedOParam .~ newValidatorScoreParameters) + makeUpdatedChainParametersRef currentParameters $ + oldCP + & (cpValidatorScoreParameters . supportedOParam .~ newValidatorScoreParameters) refMake u { currentParameters = newParameters, pendingUpdates = pendingUpdates{pValidatorScoreParametersQueue = SomeParam newpQ} } +-- | Process max lock duration updates. +-- If token-parameter updates are supported then update the Rust-managed +-- external chain-parameters component and update the pending queue. +processMaxLockDurationUpdates :: + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + Timestamp -> + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) +processMaxLockDurationUpdates timestamp updatesRef = do + updates@Updates{..} <- refLoad updatesRef + case pMaxLockDurationQueue pendingUpdates of + CFalse -> return (Map.empty, updatesRef) + CTrue queueRef -> do + queue <- refLoad queueRef + processValueUpdates timestamp queue (return (Map.empty, updatesRef)) $ \newDurationRef remainingQueue appliedUpdates -> do + newQueueRef <- refMake remainingQueue + StoreSerialized newDuration <- refLoad newDurationRef + updatedPersistentParameters <- + PCP.executeExternalChainParameterUpdate (MaxLockDurationUpdatePayload newDuration) + =<< refLoad currentParameters + updatedParametersRef <- refMake updatedPersistentParameters + updatedUpdatesRef <- + refMake + updates + { currentParameters = updatedParametersRef, + pendingUpdates = pendingUpdates{pMaxLockDurationQueue = CTrue newQueueRef} + } + return (UVMaxLockDuration <$> appliedUpdates, updatedUpdatesRef) + -- | Process the add anonymity revoker update queue. -- Ignores updates with duplicate ARs. processAddAnonymityRevokerUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> + BufferedRef (Updates pv) -> HashedBufferedRef ARS.AnonymityRevokers -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv), HashedBufferedRef ARS.AnonymityRevokers) + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv), HashedBufferedRef ARS.AnonymityRevokers) processAddAnonymityRevokerUpdates t bu hbar = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pAddAnonymityRevokerQueue pendingUpdates) @@ -1457,11 +1441,11 @@ processAddAnonymityRevokerUpdates t bu hbar = do -- | Process the add identity provider update queue. -- Ignores updates with duplicate IPs. processAddIdentityProviderUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> + BufferedRef (Updates pv) -> HashedBufferedRef IPS.IdentityProviders -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv), HashedBufferedRef IPS.IdentityProviders) + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv), HashedBufferedRef IPS.IdentityProviders) processAddIdentityProviderUpdates t bu hbip = do u@Updates{..} <- refLoad bu oldQ <- refLoad (pAddIdentityProviderQueue pendingUpdates) @@ -1514,10 +1498,10 @@ addAndAccumNonduplicateUpdates oldMap getKey toUV = foldM go (Map.empty, oldMap) -- FIXME: We may just want to keep unused protocol updates in the queue, even if their timestamps have -- elapsed. processProtocolUpdates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - BufferedRef (Updates' cpv auv) -> - m (Map.Map TransactionTime (UpdateValue cpv auv), BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (Map.Map TransactionTime (UpdateValueFor pv), BufferedRef (Updates pv)) processProtocolUpdates t bu = do u@Updates{..} <- refLoad bu protQueue <- refLoad (pProtocolQueue pendingUpdates) @@ -1545,17 +1529,17 @@ processProtocolUpdates t bu = do v <- UVProtocol . unStoreSerialized <$> refLoad r return $! Map.insert tt v m -type UpdatesWithARsAndIPs (cpv :: ChainParametersVersion) (auv :: AuthorizationsVersion) = - (BufferedRef (Updates' cpv auv), HashedBufferedRef ARS.AnonymityRevokers, HashedBufferedRef IPS.IdentityProviders) +type UpdatesWithARsAndIPs (pv :: ProtocolVersion) = + (BufferedRef (Updates pv), HashedBufferedRef ARS.AnonymityRevokers, HashedBufferedRef IPS.IdentityProviders) -- | Process all update queues. This returns a list of the updates that occurred, with their times, -- ordered by the time. processUpdateQueues :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => Timestamp -> - UpdatesWithARsAndIPs cpv auv -> - m ([(TransactionTime, UpdateValue cpv auv)], UpdatesWithARsAndIPs cpv auv) + UpdatesWithARsAndIPs pv -> + m ([(TransactionTime, UpdateValueFor pv)], UpdatesWithARsAndIPs pv) processUpdateQueues t (u0, ars, ips) = do (ms, u1) <- combine @@ -1577,7 +1561,8 @@ processUpdateQueues t (u0, ars, ips) = do processMinBlockTimeUpdates t, processBlockEnergyLimitUpdates t, processFinalizationCommitteeParametersUpdates t, - processValidationScoreParametersUpdates t + processValidationScoreParametersUpdates t, + processMaxLockDurationUpdates t ] -- AR and IP updates are handled separately to avoid adding the large objects to the 'Updates' types. @@ -1597,8 +1582,8 @@ processUpdateQueues t (u0, ars, ips) = do -- The return value is the final state of updates, and the list of -- updates. The list is in **reverse** order of the input list. combine :: - [BufferedRef (Updates' cpv auv) -> m (r, BufferedRef (Updates' cpv auv))] -> - m ([r], BufferedRef (Updates' cpv auv)) + [BufferedRef (Updates pv) -> m (r, BufferedRef (Updates pv))] -> + m ([r], BufferedRef (Updates pv)) combine = foldM ( \(ms, updates) action -> do @@ -1622,26 +1607,25 @@ processUpdateQueues t (u0, ars, ips) = do -- on a current 'Updates'. futureElectionDifficulty :: ( MonadBlobStore m, - IsChainParametersVersion cpv, - IsAuthorizationsVersion auv, - ConsensusParametersVersionFor cpv ~ 'ConsensusParametersVersion0 + IsProtocolVersion pv, + ConsensusParametersVersionFor (ChainParametersVersionFor pv) ~ 'ConsensusParametersVersion0 ) => - BufferedRef (Updates' cpv auv) -> + BufferedRef (Updates pv) -> Timestamp -> m ElectionDifficulty futureElectionDifficulty uref ts = do Updates{..} <- refLoad uref oldQ <- refLoad $ unOParam $ pElectionDifficultyQueue pendingUpdates let getCurED = do - StoreSerialized cp <- refLoad currentParameters + cp <- loadChainParametersRef currentParameters return $ cp ^. cpConsensusParameters . cpElectionDifficulty processValueUpdates ts oldQ getCurED (\newEDp _ _ -> unStoreSerialized <$> refLoad newEDp) -- | Get the protocol update status: either an effective protocol update or -- a list of pending future protocol updates. protocolUpdateStatus :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> m UQ.ProtocolUpdateStatus protocolUpdateStatus uref = do Updates{..} <- refLoad uref @@ -1653,8 +1637,8 @@ protocolUpdateStatus uref = do -- | Get whether a protocol update is effective isProtocolUpdateEffective :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> m Bool isProtocolUpdateEffective uref = do Updates{..} <- refLoad uref @@ -1664,12 +1648,12 @@ isProtocolUpdateEffective uref = do -- | Determine the next sequence number for a given update type. lookupNextUpdateSequenceNumber :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> UpdateType -> m UpdateSequenceNumber -lookupNextUpdateSequenceNumber uref uty = withCPVConstraints (chainParametersVersion @cpv) $ do +lookupNextUpdateSequenceNumber uref uty = withCPVConstraints (chainParametersVersion @(ChainParametersVersionFor pv)) $ do Updates{..} <- refLoad uref case uty of UpdateProtocol -> uqNextSequenceNumber <$> refLoad (pProtocolQueue pendingUpdates) @@ -1731,17 +1715,20 @@ lookupNextUpdateSequenceNumber uref uty = withCPVConstraints (chainParametersVer minUpdateSequenceNumber id pltUpdateSequenceNumber + UpdateMaxLockDuration -> case pMaxLockDurationQueue pendingUpdates of + CFalse -> return minUpdateSequenceNumber + CTrue queue -> uqNextSequenceNumber <$> refLoad queue -- | Enqueue an update in the appropriate queue, incrementing the sequence number of this queue. -- Note that incrementing the sequence number of updates to protocol level tokens is handled separately. enqueueUpdate :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => TransactionTime -> - UpdateValue cpv auv -> - BufferedRef (Updates' cpv auv) -> - m (BufferedRef (Updates' cpv auv)) -enqueueUpdate effectiveTime payload uref = withCPVConstraints (chainParametersVersion @cpv) $ do + UpdateValueFor pv -> + BufferedRef (Updates pv) -> + m (BufferedRef (Updates pv)) +enqueueUpdate effectiveTime payload uref = withCPVConstraints (chainParametersVersion @(ChainParametersVersionFor pv)) $ do u@Updates{pendingUpdates = p@PendingUpdates{..}} <- refLoad uref newPendingUpdates <- case payload of UVProtocol auths -> enqueue effectiveTime auths pProtocolQueue <&> \newQ -> p{pProtocolQueue = newQ} @@ -1788,15 +1775,19 @@ enqueueUpdate effectiveTime payload uref = withCPVConstraints (chainParametersVe SomeParam q -> enqueue effectiveTime v q <&> \newQ -> p{pValidatorScoreParametersQueue = SomeParam newQ} + UVMaxLockDuration v -> case pMaxLockDurationQueue of + CTrue q -> + enqueue effectiveTime v q + <&> \newQ -> p{pMaxLockDurationQueue = CTrue newQ} refMake u{pendingUpdates = newPendingUpdates} -- | 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. incrementPLTUpdateSequenceNumber :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv, SupportsCreatePLT auv ~ 'True) => - BufferedRef (Updates' cpv auv) -> - m (BufferedRef (Updates' cpv auv)) + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv, SupportsCreatePLT (AuthorizationsVersionFor pv) ~ 'True) => + BufferedRef (Updates pv) -> + m (BufferedRef (Updates pv)) incrementPLTUpdateSequenceNumber updatesRef = do currentUpdates <- refLoad updatesRef let currentSequenceNumber = uncond $ pltUpdateSequenceNumber currentUpdates @@ -1806,26 +1797,25 @@ incrementPLTUpdateSequenceNumber updatesRef = do -- any pending updates to the election difficulty from the queue. overwriteElectionDifficulty :: ( MonadBlobStore m, - IsChainParametersVersion cpv, - IsAuthorizationsVersion auv, - ConsensusParametersVersionFor cpv ~ 'ConsensusParametersVersion0 + IsProtocolVersion pv, + ConsensusParametersVersionFor (ChainParametersVersionFor pv) ~ 'ConsensusParametersVersion0 ) => ElectionDifficulty -> - BufferedRef (Updates' cpv auv) -> - m (BufferedRef (Updates' cpv auv)) + BufferedRef (Updates pv) -> + m (BufferedRef (Updates pv)) overwriteElectionDifficulty newDifficulty uref = do u@Updates{pendingUpdates = p@PendingUpdates{..}, ..} <- refLoad uref - StoreSerialized cp <- refLoad currentParameters - newCurrentParameters <- refMake $ StoreSerialized (cp & cpConsensusParameters . cpElectionDifficulty .~ newDifficulty) + cp <- loadChainParametersRef currentParameters + newCurrentParameters <- makeUpdatedChainParametersRef currentParameters (cp & cpConsensusParameters . cpElectionDifficulty .~ newDifficulty) newPendingUpdates <- clearQueue (unOParam pElectionDifficultyQueue) <&> \newQ -> p{pElectionDifficultyQueue = SomeParam newQ} refMake u{currentParameters = newCurrentParameters, pendingUpdates = newPendingUpdates} -- | Clear the protocol update and remove any pending protocol updates from -- the queue. clearProtocolUpdate :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> - m (BufferedRef (Updates' cpv auv)) + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> + m (BufferedRef (Updates pv)) clearProtocolUpdate uref = do u@Updates{pendingUpdates = p@PendingUpdates{..}} <- refLoad uref newPendingUpdates <- clearQueue pProtocolQueue <&> \newQ -> p{pProtocolQueue = newQ} @@ -1833,27 +1823,27 @@ clearProtocolUpdate uref = do -- | Get the current exchange rates, which are the Euro per NRG, micro CCD per Euro and the energy rate. lookupExchangeRates :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> m ExchangeRates lookupExchangeRates uref = do Updates{..} <- refLoad uref - StoreSerialized ChainParameters{..} <- refLoad currentParameters + ChainParameters{..} <- loadChainParametersRef currentParameters return _cpExchangeRates -- | Look up the current chain parameters. lookupCurrentParameters :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> - m (ChainParameters' cpv) + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> + m (ChainParameters pv) lookupCurrentParameters uref = do Updates{..} <- refLoad uref - unStoreSerialized <$> refLoad currentParameters + PCP.persistentChainParametersToChainParametersM =<< refLoad currentParameters -- | Look up the pending changes to the time parameters. lookupPendingTimeParameters :: - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> m [(TransactionTime, TimeParameters)] lookupPendingTimeParameters uref = do Updates{..} <- refLoad uref @@ -1863,10 +1853,10 @@ lookupPendingTimeParameters uref = do -- | Look up the pending changes to the pool parameters. lookupPendingPoolParameters :: - forall m cpv auv. - (MonadBlobStore m, IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => - BufferedRef (Updates' cpv auv) -> - m [(TransactionTime, PoolParameters cpv)] + forall m pv. + (MonadBlobStore m, IsProtocolVersion pv) => + BufferedRef (Updates pv) -> + m [(TransactionTime, PoolParameters (ChainParametersVersionFor pv))] lookupPendingPoolParameters uref = do Updates{..} <- refLoad uref - withIsPoolParametersVersionFor (chainParametersVersion @cpv) loadQueue (pPoolParametersQueue pendingUpdates) + withIsPoolParametersVersionFor (chainParametersVersion @(ChainParametersVersionFor pv)) loadQueue (pPoolParametersQueue pendingUpdates) diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs index 4987c2d501..1587371861 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Genesis.hs @@ -13,6 +13,7 @@ import qualified Concordium.Genesis.Data as GenesisData import qualified Concordium.Genesis.Data.BaseV1 as GDBaseV1 import qualified Concordium.Genesis.Data.P1 as P1 import qualified Concordium.Genesis.Data.P10 as P10 +import qualified Concordium.Genesis.Data.P11 as P11 import qualified Concordium.Genesis.Data.P2 as P2 import qualified Concordium.Genesis.Data.P3 as P3 import qualified Concordium.Genesis.Data.P4 as P4 @@ -94,6 +95,9 @@ genesisState gd = MTL.runExceptT $ case Types.protocolVersion @pv of Types.SP10 -> case gd of GenesisData.GDP10 P10.GDP10Initial{..} -> buildGenesisBlockState (CGPV1 genesisCore) genesisInitialState + Types.SP11 -> case gd of + GenesisData.GDP11 P11.GDP11Initial{..} -> + buildGenesisBlockState (CGPV1 genesisCore) genesisInitialState -------- Types ----------- diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/Migration.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/Migration.hs new file mode 100644 index 0000000000..d91223fd74 --- /dev/null +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/Migration.hs @@ -0,0 +1,315 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE EmptyCase #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Concordium.GlobalState.Persistent.Migration where + +import Concordium.Genesis.Data +import qualified Concordium.Genesis.Data.P11 as P11 +import qualified Concordium.Genesis.Data.P4 as P4 +import qualified Concordium.Genesis.Data.P6 as P6 +import qualified Concordium.Genesis.Data.P8 as P8 +import qualified Concordium.Genesis.Data.P9 as P9 +import Concordium.GlobalState.Persistent.BlobStore (SupportMigration) +import qualified Concordium.GlobalState.Persistent.BlockState.ExternalChainParameters as ECP +import Concordium.GlobalState.Persistent.BlockState.Parameters +import Concordium.Types +import Concordium.Types.Accounts +import Concordium.Types.Conditionally +import Concordium.Types.Parameters +import Concordium.Types.Updates + +-- | A witness for the migration of 'ChainParametersVersion's between two protocol versions. +-- This is used to select the correct migration for types parametrised by 'ChainParametersVersion' +-- (or its derivatives), without having to case on the 'StateMigrationParameters' directly. +data ChainParametersMigration (cpvOld :: ChainParametersVersion) (cpvNew :: ChainParametersVersion) where + ChainParametersMigrationTrivial :: ChainParametersMigration cpv cpv + ChainParametersMigrationCPV0toCPV1 :: ChainParametersMigration 'ChainParametersV0 'ChainParametersV1 + ChainParametersMigrationCPV1toCPV2 :: ChainParametersMigration 'ChainParametersV1 'ChainParametersV2 + ChainParametersMigrationCPV2toCPV3 :: ChainParametersMigration 'ChainParametersV2 'ChainParametersV3 + +-- | Get a 'ChainParametersMigration' witness from 'StateMigrationParameters'. +chainParametersMigrationFor :: + StateMigrationParameters oldpv pv -> + ChainParametersMigration (ChainParametersVersionFor oldpv) (ChainParametersVersionFor pv) +chainParametersMigrationFor StateMigrationParametersTrivial = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP1P2{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP2P3{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP3ToP4{} = ChainParametersMigrationCPV0toCPV1 +chainParametersMigrationFor StateMigrationParametersP4ToP5{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP5ToP6{} = ChainParametersMigrationCPV1toCPV2 +chainParametersMigrationFor StateMigrationParametersP6ToP7{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP7ToP8{} = ChainParametersMigrationCPV2toCPV3 +chainParametersMigrationFor StateMigrationParametersP8ToP9{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP9ToP10{} = ChainParametersMigrationTrivial +chainParametersMigrationFor StateMigrationParametersP10ToP11{} = ChainParametersMigrationTrivial + +-- | A witness for the migration of 'AccountVersion's between two protocol versions. +-- This is used to select the correct migration for types parametrised by 'AccountVersion', +-- without having to case on the 'StateMigrationParameters' directly. +data AccountTypeMigration (avOld :: AccountVersion) (avNew :: AccountVersion) where + AccountMigrationTrivial :: AccountTypeMigration av av + AccountMigrationV0ToV1 :: AccountTypeMigration 'AccountV0 'AccountV1 + AccountMigrationV1ToV2 :: AccountTypeMigration 'AccountV1 'AccountV2 + AccountMigrationV2ToV3 :: AccountTypeMigration 'AccountV2 'AccountV3 + AccountMigrationV3ToV4 :: AccountTypeMigration 'AccountV3 'AccountV4 + AccountMigrationV4ToV5 :: AccountTypeMigration 'AccountV4 'AccountV5 + +-- | Get an 'AccountTypeMigration' witness from 'StateMigrationParameters'. +accountTypeMigrationFor :: + StateMigrationParameters oldpv pv -> + AccountTypeMigration (AccountVersionFor oldpv) (AccountVersionFor pv) +accountTypeMigrationFor StateMigrationParametersTrivial = AccountMigrationTrivial +accountTypeMigrationFor StateMigrationParametersP1P2{} = AccountMigrationTrivial +accountTypeMigrationFor StateMigrationParametersP2P3{} = AccountMigrationTrivial +accountTypeMigrationFor StateMigrationParametersP3ToP4{} = AccountMigrationV0ToV1 +accountTypeMigrationFor StateMigrationParametersP4ToP5{} = AccountMigrationV1ToV2 +accountTypeMigrationFor StateMigrationParametersP5ToP6{} = AccountMigrationTrivial +accountTypeMigrationFor StateMigrationParametersP6ToP7{} = AccountMigrationV2ToV3 +accountTypeMigrationFor StateMigrationParametersP7ToP8{} = AccountMigrationV3ToV4 +accountTypeMigrationFor StateMigrationParametersP8ToP9{} = AccountMigrationV4ToV5 +accountTypeMigrationFor StateMigrationParametersP9ToP10{} = AccountMigrationTrivial +accountTypeMigrationFor StateMigrationParametersP10ToP11{} = AccountMigrationTrivial + +-- | Apply a state migration to an 'Authorizations' structure. +-- +-- [P3 to P4]: access structures for cooldown and time parameters are added. +migrateAuthorizations :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + Authorizations (AuthorizationsVersionFor oldpv) -> + Authorizations (AuthorizationsVersionFor pv) +migrateAuthorizations StateMigrationParametersTrivial auths = auths +migrateAuthorizations StateMigrationParametersP1P2 auths = auths +migrateAuthorizations StateMigrationParametersP2P3 auths = auths +migrateAuthorizations (StateMigrationParametersP3ToP4 migration) Authorizations{..} = + Authorizations + { asCooldownParameters = CTrue updateCooldownParametersAccessStructure, + asTimeParameters = CTrue updateTimeParametersAccessStructure, + .. + } + where + P4.ProtocolUpdateData{..} = P4.migrationProtocolUpdateData migration +migrateAuthorizations StateMigrationParametersP4ToP5 auths = auths +-- Note that the authorization for the consensus parameters v0 +-- are carried over to consensus parameters v1. +migrateAuthorizations StateMigrationParametersP5ToP6{} auths = auths +migrateAuthorizations StateMigrationParametersP6ToP7{} auths = auths +migrateAuthorizations StateMigrationParametersP7ToP8{} auths = auths +migrateAuthorizations (StateMigrationParametersP8ToP9 migration) Authorizations{..} = + Authorizations + { asCreatePLT = CTrue updateCreatePLTAccessStructure, + .. + } + where + P9.ProtocolUpdateData{..} = P9.migrationProtocolUpdateData migration +migrateAuthorizations StateMigrationParametersP9ToP10{} auths = auths +migrateAuthorizations (StateMigrationParametersP10ToP11 migration) Authorizations{..} = + Authorizations + { asTokenParameters = CTrue updateTokenParametersAccessStructure, + .. + } + where + P11.ProtocolUpdateData{..} = P11.migrationProtocolUpdateData migration + +-- | Apply a state migration to an 'UpdateKeysCollection' structure. +-- +-- [P3 to P4]: access structures for cooldown and time parameters are added. +migrateUpdateKeysCollection :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + UpdateKeysCollection (AuthorizationsVersionFor oldpv) -> + UpdateKeysCollection (AuthorizationsVersionFor pv) +migrateUpdateKeysCollection migration UpdateKeysCollection{..} = + UpdateKeysCollection{level2Keys = migrateAuthorizations migration level2Keys, ..} + +-- | Apply a state migration to a 'MintDistribution' structure. +-- +-- [P3 to P4]: the mint-per-slot rate is removed. +migrateMintDistribution :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + MintDistribution (MintDistributionVersionFor (ChainParametersVersionFor oldpv)) -> + MintDistribution (MintDistributionVersionFor (ChainParametersVersionFor pv)) +migrateMintDistribution migration = case chainParametersMigrationFor migration of + ChainParametersMigrationTrivial -> id + ChainParametersMigrationCPV0toCPV1 -> \MintDistribution{..} -> + MintDistribution{_mdMintPerSlot = CFalse, ..} + ChainParametersMigrationCPV1toCPV2 -> id + ChainParametersMigrationCPV2toCPV3 -> id + +-- | Apply a state migration to a 'PoolParameters' structure. +-- +-- [P3 to P4]: the new pool parameters are defined by the migration parameters. +migratePoolParameters :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + PoolParameters (ChainParametersVersionFor oldpv) -> + PoolParameters (ChainParametersVersionFor pv) +migratePoolParameters migration = case chainParametersMigrationFor migration of + ChainParametersMigrationTrivial -> id + ChainParametersMigrationCPV0toCPV1 -> case migration of + StateMigrationParametersP3ToP4 migrationData -> + \_ -> P4.updatePoolParameters (P4.migrationProtocolUpdateData migrationData) + ChainParametersMigrationCPV1toCPV2 -> id + ChainParametersMigrationCPV2toCPV3 -> id + +-- | Apply a state migration to a 'GASRewards' structure. +-- +-- This does nothing except for the P5->P6 protocol update, +-- which removes the finalization proof reward. +migrateGASRewards :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + GASRewards (GasRewardsVersionFor (ChainParametersVersionFor oldpv)) -> + GASRewards (GasRewardsVersionFor (ChainParametersVersionFor pv)) +migrateGASRewards migration = case chainParametersMigrationFor migration of + ChainParametersMigrationTrivial -> id + ChainParametersMigrationCPV0toCPV1 -> id + ChainParametersMigrationCPV1toCPV2 -> case migration of + StateMigrationParametersP5ToP6{} -> + \GASRewards{..} -> GASRewards{_gasFinalizationProof = CFalse, ..} + ChainParametersMigrationCPV2toCPV3 -> id + +-- | Apply a state migration to a 'ChainParameters' structure. +-- +-- [P3 to P4]: the new cooldown, time and pool parameters are given by the migration parameters; +-- the mint-per-slot rate is removed from the reward parameters. +-- +-- [P5 to P6]: the new consensus, finalization committee parameters are given by the migration parameters; +-- the GAS finalization proof reward is removed. +-- +-- [P7 to P8]: the new validator score parameters are given by the migration parameters. +-- +-- [P10 to P11]: the new max lock duration is given by the migration parameters +migrateChainParameters :: + forall oldpv pv t m. + (SupportMigration m t) => + StateMigrationParameters oldpv pv -> + PersistentChainParameters oldpv -> + t m (PersistentChainParameters pv) +migrateChainParameters migration = case chainParametersMigrationFor migration of + ChainParametersMigrationTrivial -> migrateChainParametersVersionUnchanged migration + ChainParametersMigrationCPV0toCPV1 -> \PersistentChainParameters{..} -> case migration of + StateMigrationParametersP3ToP4 migrationData -> + return $ + PersistentChainParameters + { pcpCooldownParameters = updateCooldownParameters, + pcpTimeParameters = SomeParam updateTimeParameters, + pcpRewardParameters = + RewardParameters + { _rpMintDistribution = migrateMintDistribution migration _rpMintDistribution, + _rpGASRewards = migrateGASRewards migration _rpGASRewards, + .. + }, + pcpPoolParameters = migratePoolParameters migration pcpPoolParameters, + pcpFinalizationCommitteeParameters = NoParam, + pcpValidatorScoreParameters = NoParam, + pcpExternalChainParameters = CFalse, + .. + } + where + RewardParameters{..} = pcpRewardParameters + P4.ProtocolUpdateData{..} = P4.migrationProtocolUpdateData migrationData + ChainParametersMigrationCPV1toCPV2 -> \PersistentChainParameters{..} -> case migration of + StateMigrationParametersP5ToP6 migrationData -> + return $ + PersistentChainParameters + { pcpConsensusParameters = updateConsensusParameters, + pcpRewardParameters = + RewardParameters + { _rpMintDistribution = migrateMintDistribution migration _rpMintDistribution, + _rpGASRewards = migrateGASRewards migration _rpGASRewards, + .. + }, + pcpPoolParameters = migratePoolParameters migration pcpPoolParameters, + pcpFinalizationCommitteeParameters = SomeParam updateFinalizationCommitteeParameters, + -- We unwrap and wrap here in order to associate the correct cpv + -- with the time parameters. + pcpTimeParameters = SomeParam $ unOParam pcpTimeParameters, + pcpValidatorScoreParameters = NoParam, + pcpExternalChainParameters = CFalse, + .. + } + where + P6.ProtocolUpdateData{..} = P6.migrationProtocolUpdateData migrationData + RewardParameters{..} = pcpRewardParameters + ChainParametersMigrationCPV2toCPV3 -> \PersistentChainParameters{..} -> case migration of + StateMigrationParametersP7ToP8 migrationData -> + return $ + PersistentChainParameters + { pcpValidatorScoreParameters = SomeParam updateValidatorScoreParameters, + pcpTimeParameters = SomeParam $ unOParam pcpTimeParameters, + pcpFinalizationCommitteeParameters = SomeParam $ unOParam pcpFinalizationCommitteeParameters, + pcpPoolParameters = migratePoolParameters migration pcpPoolParameters, + pcpRewardParameters = + RewardParameters + { _rpMintDistribution = migrateMintDistribution migration _rpMintDistribution, + _rpGASRewards = migrateGASRewards migration _rpGASRewards, + .. + }, + pcpExternalChainParameters = CFalse, + .. + } + where + P8.ProtocolUpdateData{..} = P8.migrationProtocolUpdateData migrationData + RewardParameters{..} = pcpRewardParameters + +-- | Migrate persistent chain parameters when the chain parameter version is unchanged. +migrateChainParametersVersionUnchanged :: + forall oldpv pv t m. + (SupportMigration m t) => + StateMigrationParameters oldpv pv -> + PersistentChainParameters oldpv -> + t m (PersistentChainParameters pv) +migrateChainParametersVersionUnchanged StateMigrationParametersTrivial params = return params +migrateChainParametersVersionUnchanged StateMigrationParametersP1P2 PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged StateMigrationParametersP2P3 PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged StateMigrationParametersP4ToP5 PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged StateMigrationParametersP6ToP7 PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged StateMigrationParametersP8ToP9{} PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged StateMigrationParametersP9ToP10{} PersistentChainParameters{..} = + return PersistentChainParameters{pcpExternalChainParameters = CFalse, ..} +migrateChainParametersVersionUnchanged (StateMigrationParametersP10ToP11 migration) PersistentChainParameters{..} = do + newExternalChainParameters <- CTrue <$> ECP.p11NewExternalChainParameters updateMaxLockDuration + return PersistentChainParameters{pcpExternalChainParameters = newExternalChainParameters, ..} + where + P11.ProtocolUpdateData{..} = P11.migrationProtocolUpdateData migration +migrateChainParametersVersionUnchanged _ _ = error "migrateChainParametersVersionUnchanged called for non-trivial chain parameter version migration" + +-- | Migrate time of the effective change from V0 to V1 accounts. Currently this +-- translates times relative to genesis to times relative to the unix epoch. +migratePendingChangeEffective :: P4.StateMigrationData -> PendingChangeEffective 'AccountV0 -> PendingChangeEffective 'AccountV1 +migratePendingChangeEffective P4.StateMigrationData{..} (PendingChangeEffectiveV0 eff) = + PendingChangeEffectiveV1 $ + addDuration + migrationPreviousGenesisTime + (migrationPreviousEpochDuration * fromIntegral eff) + +-- | Migrate the stake pending change from the representation used by protocol +-- version @oldpv@ to the representation used by the protocol version @pv@. The +-- migration parameters supply auxiliary data needed for the migration. +migrateStakePendingChange :: + forall oldpv pv. + StateMigrationParameters oldpv pv -> + StakePendingChange (AccountVersionFor oldpv) -> + StakePendingChange (AccountVersionFor pv) +migrateStakePendingChange migration = case accountTypeMigrationFor migration of + AccountMigrationTrivial -> id + AccountMigrationV0ToV1 -> case migration of + StateMigrationParametersP3ToP4 migrationData -> \case + NoChange -> NoChange + ReduceStake amnt eff -> ReduceStake amnt (migratePendingChangeEffective migrationData eff) + RemoveStake eff -> RemoveStake (migratePendingChangeEffective migrationData eff) + AccountMigrationV1ToV2 -> fmap coercePendingChangeEffectiveV1 + AccountMigrationV2ToV3 -> const NoChange + AccountMigrationV3ToV4 -> \NoChange -> NoChange + AccountMigrationV4ToV5 -> \NoChange -> NoChange diff --git a/concordium-consensus/src/Concordium/GlobalState/Persistent/ReleaseSchedule.hs b/concordium-consensus/src/Concordium/GlobalState/Persistent/ReleaseSchedule.hs index 5f9f8bbe3d..4676cf7ab5 100644 --- a/concordium-consensus/src/Concordium/GlobalState/Persistent/ReleaseSchedule.hs +++ b/concordium-consensus/src/Concordium/GlobalState/Persistent/ReleaseSchedule.hs @@ -282,6 +282,27 @@ type family RSAccountRef pv where RSAccountRef 'P4 = AccountAddress RSAccountRef _ = AccountIndex +-- | A GADT that witnesses the type of account reference used in the release schedule for a given +-- protocol version. +data RSAccountRefType (pv :: ProtocolVersion) where + RSAccountRefTypeAccountAddress :: (RSAccountRef pv ~ AccountAddress) => RSAccountRefType pv + RSAccountRefTypeAccountIndex :: (RSAccountRef pv ~ AccountIndex) => RSAccountRefType pv + +-- | Get the 'RSAccountRefType' for a given protocol version. +releaseScheduleAccountRefType :: forall pv. (IsProtocolVersion pv) => RSAccountRefType pv +releaseScheduleAccountRefType = case protocolVersion @pv of + SP1 -> RSAccountRefTypeAccountAddress + SP2 -> RSAccountRefTypeAccountAddress + SP3 -> RSAccountRefTypeAccountAddress + SP4 -> RSAccountRefTypeAccountAddress + SP5 -> RSAccountRefTypeAccountIndex + SP6 -> RSAccountRefTypeAccountIndex + SP7 -> RSAccountRefTypeAccountIndex + SP8 -> RSAccountRefTypeAccountIndex + SP9 -> RSAccountRefTypeAccountIndex + SP10 -> RSAccountRefTypeAccountIndex + SP11 -> RSAccountRefTypeAccountIndex + -- | A top-level release schedule used for a particular protocol version. data ReleaseSchedule (pv :: ProtocolVersion) where -- | A release schedule for protocol versions 'P1' to 'P4'. @@ -300,17 +321,9 @@ deriving instance (IsProtocolVersion pv) => Show (ReleaseSchedule pv) instance (MonadBlobStore m, IsProtocolVersion pv) => BlobStorable m (ReleaseSchedule pv) where storeUpdate (ReleaseScheduleP0 rs) = second ReleaseScheduleP0 <$> storeUpdate rs storeUpdate (ReleaseScheduleP5 rs) = second ReleaseScheduleP5 <$> storeUpdate rs - load = case protocolVersion @pv of - SP1 -> fmap ReleaseScheduleP0 <$> load - SP2 -> fmap ReleaseScheduleP0 <$> load - SP3 -> fmap ReleaseScheduleP0 <$> load - SP4 -> fmap ReleaseScheduleP0 <$> load - SP5 -> fmap ReleaseScheduleP5 <$> load - SP6 -> fmap ReleaseScheduleP5 <$> load - SP7 -> fmap ReleaseScheduleP5 <$> load - SP8 -> fmap ReleaseScheduleP5 <$> load - SP9 -> fmap ReleaseScheduleP5 <$> load - SP10 -> fmap ReleaseScheduleP5 <$> load + load = case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> fmap ReleaseScheduleP0 <$> load + RSAccountRefTypeAccountIndex -> fmap ReleaseScheduleP5 <$> load instance (MonadBlobStore m) => Cacheable m (ReleaseSchedule pv) where cache (ReleaseScheduleP0 rs) = ReleaseScheduleP0 <$> cache rs @@ -333,17 +346,9 @@ instance (MonadBlobStore m) => ReleaseScheduleOperations m (ReleaseSchedule pv) -- | Construct an empty release schedule. emptyReleaseSchedule :: forall m pv. (IsProtocolVersion pv, MonadBlobStore m) => m (ReleaseSchedule pv) -emptyReleaseSchedule = case protocolVersion @pv of - SP1 -> rsP0 - SP2 -> rsP0 - SP3 -> rsP0 - SP4 -> rsP0 - SP5 -> rsP1 - SP6 -> rsP1 - SP7 -> rsP1 - SP8 -> rsP1 - SP9 -> rsP1 - SP10 -> rsP1 +emptyReleaseSchedule = case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> rsP0 + RSAccountRefTypeAccountIndex -> rsP1 where rsP0 :: (RSAccountRef pv ~ AccountAddress) => m (ReleaseSchedule pv) rsP0 = do @@ -383,17 +388,9 @@ trivialReleaseScheduleMigration :: forall m pv. (IsProtocolVersion pv) => ReleaseScheduleMigration m pv pv -trivialReleaseScheduleMigration = case protocolVersion @pv of - SP1 -> RSMLegacyToLegacy - SP2 -> RSMLegacyToLegacy - SP3 -> RSMLegacyToLegacy - SP4 -> RSMLegacyToLegacy - SP5 -> RSMNewToNew - SP6 -> RSMNewToNew - SP7 -> RSMNewToNew - SP8 -> RSMNewToNew - SP9 -> RSMNewToNew - SP10 -> RSMNewToNew +trivialReleaseScheduleMigration = case releaseScheduleAccountRefType @pv of + RSAccountRefTypeAccountAddress -> RSMLegacyToLegacy + RSAccountRefTypeAccountIndex -> RSMNewToNew -- | Migrate a release schedule from one protocol version to another, given by a -- 'ReleaseScheduleMigration'. diff --git a/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs b/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs index e9ad4e5e44..d32f72deaf 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/Scheduler.hs @@ -32,7 +32,7 @@ import Concordium.GlobalState.Types import Concordium.KonsensusV1.LeaderElection import Concordium.Kontrol.Bakers import Concordium.Scheduler -import qualified Concordium.Scheduler.EnvironmentImplementation as EnvImpl +import qualified Concordium.Scheduler.Environment as Env import Concordium.Scheduler.TreeStateEnvironment (FreeTransactionCounts (countAccountCreation), countFreeTransactions, distributeRewards, doBlockRewardP4, doCalculatePaydayMintAmounts) import Concordium.Scheduler.Types import qualified Concordium.TransactionVerification as TVer @@ -562,18 +562,18 @@ constructBlockTransactions runtimeParams startTime transTable pendingTable block -- The block energy limit and account creation limit are taken from the current chain parameters. chainParams <- bsoGetChainParameters theState0 let context = - EnvImpl.ContextState + Env.ContextState { _chainMetadata = ChainMetadata blockTimestamp, _maxBlockEnergy = chainParams ^. cpConsensusParameters . cpBlockEnergyLimit, _accountCreationLimit = chainParams ^. cpAccountCreationLimit } -- Filter the transactions, executing the valid ones. (ft, finState) <- - EnvImpl.runSchedulerT + Env.runSchedulerT (filterTransactions maxBlockSize timeout transactionGroups) context - (EnvImpl.makeInitialSchedulerState theState0) - let theState1 = finState ^. EnvImpl.ssBlockState + (Env.makeInitialSchedulerState theState0) + let theState1 = finState ^. Env.ssBlockState -- Record the transaction outcomes. theState2 <- bsoSetTransactionOutcomes theState1 (snd <$> ftAdded ft) let result = @@ -583,9 +583,9 @@ constructBlockTransactions runtimeParams startTime transTable pendingTable block { trpFreeTransactionCounts = countFreeTransactions (fst . fst <$> ftAdded ft) False, trpTransactionFees = - finState ^. EnvImpl.ssExecutionCosts + finState ^. Env.ssExecutionCosts }, - terEnergyUsed = finState ^. EnvImpl.ssEnergyUsed, + terEnergyUsed = finState ^. Env.ssEnergyUsed, terBlockState = theState2 } return (ft, result) @@ -618,18 +618,18 @@ executeBlockTransactions blockTimestamp transactions theState0 = do return $ Left $ Just ExceedsMaxCredentialDeployments else do let context = - EnvImpl.ContextState + Env.ContextState { _chainMetadata = ChainMetadata blockTimestamp, _maxBlockEnergy = chainParams ^. cpConsensusParameters . cpBlockEnergyLimit, _accountCreationLimit = accountCreationLim } - let initState = EnvImpl.makeInitialSchedulerState theState0 + let initState = Env.makeInitialSchedulerState theState0 (res, finState) <- - EnvImpl.runSchedulerT + Env.runSchedulerT (runTransactions ((_2 %~ Just) <$> transactions)) context initState - let theState1 = finState ^. EnvImpl.ssBlockState + let theState1 = finState ^. Env.ssBlockState case res of Left failKind -> do dropUpdatableBlockState theState1 @@ -641,9 +641,9 @@ executeBlockTransactions blockTimestamp transactions theState0 = do { terTransactionRewardParameters = TransactionRewardParameters { trpFreeTransactionCounts = freeCounts, - trpTransactionFees = finState ^. EnvImpl.ssExecutionCosts + trpTransactionFees = finState ^. Env.ssExecutionCosts }, - terEnergyUsed = finState ^. EnvImpl.ssEnergyUsed, + terEnergyUsed = finState ^. Env.ssEnergyUsed, terBlockState = theState2 } return $ Right result diff --git a/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs b/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs index 1ea905ac51..9e7a6d3a5a 100644 --- a/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs +++ b/concordium-consensus/src/Concordium/KonsensusV1/TestMonad.hs @@ -26,6 +26,7 @@ import Concordium.Types import qualified Concordium.Genesis.Data.BaseV1 as BaseV1 import qualified Concordium.Genesis.Data.P10 as P10 +import qualified Concordium.Genesis.Data.P11 as P11 import qualified Concordium.Genesis.Data.P6 as P6 import qualified Concordium.Genesis.Data.P7 as P7 import qualified Concordium.Genesis.Data.P8 as P8 @@ -34,7 +35,7 @@ import qualified Concordium.GlobalState.AccountMap.LMDB as LMDBAccountMap import Concordium.GlobalState.BlockState import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import Concordium.GlobalState.Parameters ( - GenesisData (GDP10, GDP6, GDP7, GDP8, GDP9), + GenesisData (..), defaultRuntimeParameters, genesisBlockHash, ) @@ -155,6 +156,7 @@ genesisCore = case protocolVersion @pv of SP8 -> \(GDP8 P8.GDP8Initial{genesisCore = core}) -> core SP9 -> \(GDP9 P9.GDP9Initial{genesisCore = core}) -> core SP10 -> \(GDP10 P10.GDP10Initial{genesisCore = core}) -> core + SP11 -> \(GDP11 P11.GDP11Initial{genesisCore = core}) -> core -- | Run an operation in the 'TestMonad' with the given baker, time and genesis data. -- This sets up a temporary blob store for the block state that is deleted after use. diff --git a/concordium-consensus/src/Concordium/MultiVersion.hs b/concordium-consensus/src/Concordium/MultiVersion.hs index a429a4a48c..dda97ab793 100644 --- a/concordium-consensus/src/Concordium/MultiVersion.hs +++ b/concordium-consensus/src/Concordium/MultiVersion.hs @@ -575,7 +575,9 @@ newtype MVR finconf a = MVR {runMVR :: MultiVersionRunner finconf -> IO a} instance MonadLogger (MVR finconf) where logEvent src lvl msg = MVR $ \mvr -> mvLog mvr src lvl msg + logEventIO = MVR $ \mvr -> return $ mvLog mvr {-# INLINE logEvent #-} + {-# INLINE logEventIO #-} -- | Catch and log exceptions in the 'MVR' monad. -- Returns a specified value in the event of an exception. diff --git a/concordium-consensus/src/Concordium/ProtocolUpdate/P10.hs b/concordium-consensus/src/Concordium/ProtocolUpdate/P10.hs index cd3f70ef68..8fdf81205f 100644 --- a/concordium-consensus/src/Concordium/ProtocolUpdate/P10.hs +++ b/concordium-consensus/src/Concordium/ProtocolUpdate/P10.hs @@ -10,33 +10,41 @@ module Concordium.ProtocolUpdate.P10 ( import Control.Monad.State import qualified Data.HashMap.Strict as HM -import Data.Serialize +import qualified Data.Serialize as S import qualified Concordium.Crypto.SHA256 as SHA256 import Concordium.Types import Concordium.Types.Updates +import qualified Concordium.Genesis.Data.P11 as P11 import Concordium.GlobalState.BlockState import qualified Concordium.GlobalState.Persistent.BlockState as PBS import Concordium.GlobalState.Types import qualified Concordium.GlobalState.Types as GSTypes import Concordium.KonsensusV1.TreeState.Implementation import Concordium.KonsensusV1.TreeState.Types +import qualified Concordium.ProtocolUpdate.P10.ProtocolP11 as ProtocolP11 import qualified Concordium.ProtocolUpdate.P10.Reboot as Reboot -- | Updates that are supported from protocol version P10. -data Update = Reboot +data Update + = Reboot + | ProtocolP11 P11.ProtocolUpdateData deriving (Show) -- | Hash map for resolving updates from their specification hash. -updates :: HM.HashMap SHA256.Hash (Get Update) -updates = HM.fromList [(Reboot.updateHash, return Reboot)] +updates :: HM.HashMap SHA256.Hash (S.Get Update) +updates = + HM.fromList + [ (Reboot.updateHash, return Reboot), + (ProtocolP11.updateHash, ProtocolP11 <$> S.get) + ] -- | Determine if a 'ProtocolUpdate' corresponds to a supported update type. checkUpdate :: ProtocolUpdate -> Either String Update checkUpdate ProtocolUpdate{..} = case HM.lookup puSpecificationHash updates of Nothing -> Left "Specification hash does not correspond to a known protocol update." - Just updateGet -> case runGet updateGet puSpecificationAuxiliaryData of + Just updateGet -> case S.runGet updateGet puSpecificationAuxiliaryData of Left err -> Left $! "Could not deserialize auxiliary data: " ++ err Right update -> return update @@ -53,9 +61,11 @@ updateRegenesis :: BlockPointer (MPV m) -> m (PVInit m) updateRegenesis Reboot = Reboot.updateRegenesis +updateRegenesis (ProtocolP11 protocolUpdateData) = ProtocolP11.updateRegenesis protocolUpdateData -- | Determine the protocol version the update will update to. updateNextProtocolVersion :: Update -> SomeProtocolVersion updateNextProtocolVersion Reboot{} = SomeProtocolVersion SP10 +updateNextProtocolVersion ProtocolP11{} = SomeProtocolVersion SP11 diff --git a/concordium-consensus/src/Concordium/ProtocolUpdate/P10/ProtocolP11.hs b/concordium-consensus/src/Concordium/ProtocolUpdate/P10/ProtocolP11.hs new file mode 100644 index 0000000000..6842e2d24a --- /dev/null +++ b/concordium-consensus/src/Concordium/ProtocolUpdate/P10/ProtocolP11.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} + +-- | This module implements the P10.ProtocolP11 protocol update. +-- This protocol update is valid at protocol version P10, and updates +-- to protocol version P11. +-- +-- This produces a new 'RegenesisDataP11' using the 'GDP11RegenesisFromP10' constructor, +-- as follows: +-- +-- * 'genesisCore': +-- +-- * 'genesisTime' is the timestamp of the last finalized block of the previous chain. +-- * 'genesisEpochDuration' is taken from the previous genesis. +-- * 'genesisSignatureThreshold' is taken from the previous genesis. +-- +-- * 'genesisFirstGenesis' is either: +-- +-- * the hash of the genesis block of the previous chain, if it is a 'GDP10Initial'; or +-- * the 'genesisFirstGenesis' value of the genesis block of the previous chain, if it +-- is a 'GDP10Regenesis'. +-- +-- * 'genesisPreviousGenesis' is the hash of the previous genesis block. +-- +-- * 'genesisTerminalBlock' is the hash of the last finalized block of the previous chain. +-- +-- * 'genesisStateHash' is the state hash of the last finalized block of the previous chain. +-- +-- * 'genesisMigration' is derived from the protocol update auxiliary data. +-- +-- The block state is taken from the last finalized block of the previous chain. It is updated +-- as part of the state migration, which makes the following changes: +-- +-- * The seed state is migrated as follows: +-- +-- * The current epoch is reset to zero. +-- * The current and updated leadership election nonce are set to the hash of +-- @"Regenesis" <> encode oldUpdatedNonce@. +-- * The trigger block time is kept the same, meaning that the epoch will transition as soon +-- as possible. +-- * The epoch transition triggered flag is set. +-- * The shutdown triggered flag is cleared. +-- +-- * The old current epoch is subtracted from the next payday epoch. +-- +-- * The protocol update queue is emptied during the migration. +-- +-- Note that, the initial epoch of the new chain is not considered +-- a new epoch for the purposes of block rewards and baker/finalization committee determination. +-- In particular, the timing of the next payday will be the same as if the protocol update +-- had not happened. (For instance, if it would have happened at the start of the next epoch +-- prior to the protocol update, after the update it will happen at the start of epoch 1. +-- The trigger block time in epoch 0 of the new consensus is the same as the trigger block +-- time in the final epoch of the old consensus.) +-- Furthermore, the bakers from the final epoch of the previous chain are also the bakers for the +-- initial epoch of the new chain. +module Concordium.ProtocolUpdate.P10.ProtocolP11 where + +import Control.Monad.State +import Lens.Micro.Platform + +import qualified Concordium.Crypto.SHA256 as SHA256 +import qualified Concordium.Genesis.Data as GenesisData +import qualified Concordium.Genesis.Data.BaseV1 as BaseV1 +import qualified Concordium.Genesis.Data.P11 as P11 +import Concordium.GlobalState.BlockState +import qualified Concordium.GlobalState.Persistent.BlockState as PBS +import Concordium.GlobalState.Types +import qualified Concordium.GlobalState.Types as GSTypes +import qualified Concordium.KonsensusV1.TreeState.Implementation as TreeState +import Concordium.KonsensusV1.TreeState.Types +import Concordium.KonsensusV1.Types +import Concordium.Types.HashableTo (getHash) +import Concordium.Types.ProtocolVersion + +-- | The hash that identifies a update from P10 to P11 protocol. +-- TODO: Replace this provisional value with the P11 specification hash when available. +updateHash :: SHA256.Hash +updateHash = read "000000000000000000000000000000000000000000000000000000000000000b" + +-- | Construct the genesis data for a P10.ProtocolP11 update. +-- This takes the terminal block of the old chain which is used as the basis for constructing +-- the new genesis block. +updateRegenesis :: + ( MPV m ~ 'P10, + BlockStateStorage m, + MonadState (TreeState.SkovData (MPV m)) m, + GSTypes.BlockState m ~ PBS.HashedPersistentBlockState (MPV m) + ) => + P11.ProtocolUpdateData -> + BlockPointer 'P10 -> + m (PVInit m) +updateRegenesis protocolUpdateData terminalBlock = do + let regenesisTime = blockTimestamp terminalBlock + gMetadata <- use TreeState.genesisMetadata + BaseV1.CoreGenesisParametersV1{..} <- gmParameters <$> use TreeState.genesisMetadata + let core = + BaseV1.CoreGenesisParametersV1 + { BaseV1.genesisTime = regenesisTime, + .. + } + let genesisFirstGenesis = gmFirstGenesisHash gMetadata + genesisPreviousGenesis = gmCurrentGenesisHash gMetadata + genesisTerminalBlock = getHash terminalBlock + let regenesisBlockState = bpState terminalBlock + genesisStateHash <- getStateHash regenesisBlockState + let genesisMigration = + P11.StateMigrationData + { migrationProtocolUpdateData = protocolUpdateData + } + let newGenesis = GenesisData.RGDP11 $ P11.GDP11RegenesisFromP10{genesisRegenesis = BaseV1.RegenesisDataV1{genesisCore = core, ..}, ..} + return (PVInit newGenesis (GenesisData.StateMigrationParametersP10ToP11 genesisMigration) (bmHeight $ bpInfo terminalBlock)) diff --git a/concordium-consensus/src/Concordium/ProtocolUpdate/P11.hs b/concordium-consensus/src/Concordium/ProtocolUpdate/P11.hs new file mode 100644 index 0000000000..47c0f4a673 --- /dev/null +++ b/concordium-consensus/src/Concordium/ProtocolUpdate/P11.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeFamilies #-} + +module Concordium.ProtocolUpdate.P11 ( + Update (..), + checkUpdate, + updateRegenesis, + updateNextProtocolVersion, +) where + +import Control.Monad.State +import qualified Data.HashMap.Strict as HM +import Data.Serialize + +import qualified Concordium.Crypto.SHA256 as SHA256 +import Concordium.Types +import Concordium.Types.Updates + +import Concordium.GlobalState.BlockState +import qualified Concordium.GlobalState.Persistent.BlockState as PBS +import Concordium.GlobalState.Types +import qualified Concordium.GlobalState.Types as GSTypes +import Concordium.KonsensusV1.TreeState.Implementation +import Concordium.KonsensusV1.TreeState.Types +import qualified Concordium.ProtocolUpdate.P11.Reboot as Reboot + +-- | Updates that are supported from protocol version P11. +data Update = Reboot + deriving (Show) + +-- | Hash map for resolving updates from their specification hash. +updates :: HM.HashMap SHA256.Hash (Get Update) +updates = HM.fromList [(Reboot.updateHash, return Reboot)] + +-- | Determine if a 'ProtocolUpdate' corresponds to a supported update type. +checkUpdate :: ProtocolUpdate -> Either String Update +checkUpdate ProtocolUpdate{..} = case HM.lookup puSpecificationHash updates of + Nothing -> Left "Specification hash does not correspond to a known protocol update." + Just updateGet -> case runGet updateGet puSpecificationAuxiliaryData of + Left err -> Left $! "Could not deserialize auxiliary data: " ++ err + Right update -> return update + +-- | Construct the genesis data for a P11 update. +updateRegenesis :: + ( MPV m ~ 'P11, + BlockStateStorage m, + MonadState (SkovData (MPV m)) m, + GSTypes.BlockState m ~ PBS.HashedPersistentBlockState (MPV m) + ) => + -- | The update taking effect. + Update -> + -- | The terminal block of the old chain. + BlockPointer (MPV m) -> + m (PVInit m) +updateRegenesis Reboot = Reboot.updateRegenesis + +-- | Determine the protocol version the update will update to. +updateNextProtocolVersion :: + Update -> + SomeProtocolVersion +updateNextProtocolVersion Reboot{} = SomeProtocolVersion SP11 diff --git a/concordium-consensus/src/Concordium/ProtocolUpdate/P11/Reboot.hs b/concordium-consensus/src/Concordium/ProtocolUpdate/P11/Reboot.hs new file mode 100644 index 0000000000..bfd8c9f0ea --- /dev/null +++ b/concordium-consensus/src/Concordium/ProtocolUpdate/P11/Reboot.hs @@ -0,0 +1,111 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeFamilies #-} + +-- | This module implements the P11.Reboot protocol update. +-- This protocol update is valid at protocol version P11, and updates +-- to protocol version P11. +-- This produces a new 'RegenesisDataP11 using the 'GDP11Regenesis' constructor, +-- as follows: +-- +-- * 'genesisCore': +-- +-- * 'genesisTime' is the timestamp of the last finalized block of the previous chain. +-- * 'genesisEpochDuration' is taken from the previous genesis. +-- * 'genesisSignatureThreshold' is taken from the previous genesis. +-- +-- * 'genesisFirstGenesis' is either: +-- +-- * the hash of the genesis block of the previous chain, if it is a 'GDP11Initial'; or +-- * the 'genesisFirstGenesis' value of the genesis block of the previous chain, if it +-- is a 'GDP11Regenesis'. +-- +-- * 'genesisPreviousGenesis' is the hash of the previous genesis block. +-- +-- * 'genesisTerminalBlock' is the hash of the last finalized block of the previous chain. +-- +-- * 'genesisStateHash' is the state hash of the last finalized block of the previous chain. +-- +-- The block state is taken from the last finalized block of the previous chain. It is updated +-- as part of the state migration, which makes the following changes: +-- +-- * The seed state is migrated as follows: +-- +-- * The current epoch is reset to zero. +-- * The current and updated leadership election nonce are set to the hash of +-- @"Regenesis" <> encode oldUpdatedNonce@. +-- * The trigger block time is kept the same, meaning that the epoch will transition as soon +-- as possible. +-- * The epoch transition triggered flag is set. +-- * The shutdown triggered flag is cleared. +-- +-- * The old current epoch is subtracted from the next payday epoch. +-- +-- * The protocol update queue is emptied during the migration. +-- +-- Note that, the initial epoch of the new chain is not considered +-- a new epoch for the purposes of block rewards and baker/finalization committee determination. +-- In particular, the timing of the next payday will be the same as if the protocol update +-- had not happened. (For instance, if it would have happened at the start of the next epoch +-- prior to the protocol update, after the update it will happen at the start of epoch 1. +-- The trigger block time in epoch 0 of the new consensus is the same as the trigger block +-- time in the final epoch of the old consensus.) +-- Furthermore, the bakers from the final epoch of the previous chain are also the bakers for the +-- initial epoch of the new chain. +module Concordium.ProtocolUpdate.P11.Reboot where + +import Control.Monad.State +import Lens.Micro.Platform + +import qualified Concordium.Crypto.SHA256 as SHA256 +import qualified Concordium.Genesis.Data as GenesisData +import qualified Concordium.Genesis.Data.BaseV1 as BaseV1 +import qualified Concordium.Genesis.Data.P11 as P11 +import Concordium.GlobalState.BlockState +import qualified Concordium.GlobalState.Persistent.BlockState as PBS +import Concordium.GlobalState.Types +import qualified Concordium.GlobalState.Types as GSTypes +import Concordium.KonsensusV1.TreeState.Implementation +import Concordium.KonsensusV1.TreeState.Types +import Concordium.KonsensusV1.Types +import Concordium.Types.HashableTo (getHash) +import Concordium.Types.ProtocolVersion + +-- | The hash that identifies the P11.Reboot update: +-- e135d02624bcf91d8184c6746f6b2fc2e869df0b2716693e47e5ece8ec4d9704 +updateHash :: SHA256.Hash +updateHash = SHA256.hash "P11.Reboot" + +-- | Construct the genesis data for a P11.Reboot update. +-- This takes the terminal block of the old chain which is used as the basis for constructing +-- the new genesis block. +updateRegenesis :: + ( MPV m ~ 'P11, + BlockStateStorage m, + MonadState (SkovData (MPV m)) m, + GSTypes.BlockState m ~ PBS.HashedPersistentBlockState (MPV m) + ) => + -- | The terminal block of the old chain. + BlockPointer 'P11 -> + m (PVInit m) +updateRegenesis terminal = do + -- Genesis time is the timestamp of the terminal block + let regenesisTime = blockTimestamp terminal + -- Core parameters are derived from the old genesis, apart from genesis time which is set for + -- the time of the terminal block. + gMetadata <- use genesisMetadata + BaseV1.CoreGenesisParametersV1{..} <- gmParameters <$> use genesisMetadata + let core = + BaseV1.CoreGenesisParametersV1 + { BaseV1.genesisTime = regenesisTime, + .. + } + -- genesisFirstGenesis is the block hash of the previous genesis, if it is initial, + -- or the genesisFirstGenesis of the previous genesis otherwise. + let genesisFirstGenesis = gmFirstGenesisHash gMetadata + genesisPreviousGenesis = gmCurrentGenesisHash gMetadata + genesisTerminalBlock = getHash terminal + let regenesisBlockState = bpState terminal + genesisStateHash <- getStateHash regenesisBlockState + let newGenesis = GenesisData.RGDP11 $ P11.GDP11Regenesis{genesisRegenesis = BaseV1.RegenesisDataV1{genesisCore = core, ..}} + return (PVInit newGenesis GenesisData.StateMigrationParametersTrivial (bmHeight $ bpInfo terminal)) diff --git a/concordium-consensus/src/Concordium/ProtocolUpdate/V1.hs b/concordium-consensus/src/Concordium/ProtocolUpdate/V1.hs index 5a1c070156..18d3126a43 100644 --- a/concordium-consensus/src/Concordium/ProtocolUpdate/V1.hs +++ b/concordium-consensus/src/Concordium/ProtocolUpdate/V1.hs @@ -18,6 +18,7 @@ import qualified Concordium.GlobalState.Types as GSTypes import Concordium.KonsensusV1.TreeState.Implementation import Concordium.KonsensusV1.TreeState.Types import qualified Concordium.ProtocolUpdate.P10 as P10 +import qualified Concordium.ProtocolUpdate.P11 as P11 import qualified Concordium.ProtocolUpdate.P6 as P6 import qualified Concordium.ProtocolUpdate.P7 as P7 import qualified Concordium.ProtocolUpdate.P8 as P8 @@ -30,6 +31,7 @@ data Update (pv :: ProtocolVersion) where UpdateP8 :: P8.Update -> Update 'P8 UpdateP9 :: P9.Update -> Update 'P9 UpdateP10 :: P10.Update -> Update 'P10 + UpdateP11 :: P11.Update -> Update 'P11 instance Show (Update pv) where show (UpdateP6 u) = "P6." ++ show u @@ -37,6 +39,7 @@ instance Show (Update pv) where show (UpdateP8 u) = "P8." ++ show u show (UpdateP9 u) = "P9." ++ show u show (UpdateP10 u) = "P10." ++ show u + show (UpdateP11 u) = "P11." ++ show u -- | Determine if a 'ProtocolUpdate' corresponds to a supported update type. checkUpdate :: forall pv. (IsProtocolVersion pv) => ProtocolUpdate -> Either String (Update pv) @@ -53,6 +56,7 @@ checkUpdate = case protocolVersion @pv of SP8 -> fmap UpdateP8 . P8.checkUpdate SP9 -> fmap UpdateP9 . P9.checkUpdate SP10 -> fmap UpdateP10 . P10.checkUpdate + SP11 -> fmap UpdateP11 . P11.checkUpdate -- | Construct the genesis data for a P1 update. updateRegenesis :: @@ -70,6 +74,7 @@ updateRegenesis (UpdateP7 u) = P7.updateRegenesis u updateRegenesis (UpdateP8 u) = P8.updateRegenesis u updateRegenesis (UpdateP9 u) = P9.updateRegenesis u updateRegenesis (UpdateP10 u) = P10.updateRegenesis u +updateRegenesis (UpdateP11 u) = P11.updateRegenesis u -- | Determine the next protocol version for the given update. Although the same -- information can be retrieved from 'updateRegenesis', this is more efficient @@ -82,3 +87,4 @@ updateNextProtocolVersion (UpdateP7 u) = P7.updateNextProtocolVersion u updateNextProtocolVersion (UpdateP8 u) = P8.updateNextProtocolVersion u updateNextProtocolVersion (UpdateP9 u) = P9.updateNextProtocolVersion u updateNextProtocolVersion (UpdateP10 u) = P10.updateNextProtocolVersion u +updateNextProtocolVersion (UpdateP11 u) = P11.updateNextProtocolVersion u diff --git a/concordium-consensus/src/Concordium/Queries.hs b/concordium-consensus/src/Concordium/Queries.hs index 9a976bdd79..d768134171 100644 --- a/concordium-consensus/src/Concordium/Queries.hs +++ b/concordium-consensus/src/Concordium/Queries.hs @@ -46,10 +46,12 @@ import Concordium.Types.Execution ( ) import Concordium.Types.HashableTo import Concordium.Types.IdentityProviders +import qualified Concordium.Types.Locks as Locks import Concordium.Types.Option import Concordium.Types.Parameters import Concordium.Types.Queries hiding (PassiveCommitteeInfo (..), bakerId) import qualified Concordium.Types.Queries.KonsensusV1 as QueriesKonsensusV1 +import qualified Concordium.Types.Queries.Locks as Locks import qualified Concordium.Types.Queries.Tokens as Tokens import Concordium.Types.SeedState import Concordium.Types.Transactions @@ -89,7 +91,17 @@ import qualified Concordium.KonsensusV1.Types as SkovV1 import Concordium.Kontrol import Concordium.Kontrol.BestBlock import Concordium.MultiVersion -import Concordium.Scheduler.ProtocolLevelTokens.Queries (QueryTokenInfoError, queryAccountTokens, queryTokenInfo) +import Concordium.Scheduler.ProtocolLevelTokens.Queries ( + QueryLockError, + QueryTokenModuleError, + SerializedLockId, + queryAccountTokens, + queryLockInfo, + queryLockList, + queryPLTList, + queryTokenAuthorizations, + queryTokenInfo, + ) import Concordium.Skov as Skov ( SkovQueryMonad (getBlocksAtHeight), evalSkovT, @@ -904,6 +916,7 @@ getBlockPendingUpdates = liftSkovQueryStateBHI query SAuthorizationsVersion0 -> queueMapper PUELevel2KeysV0 _pLevel2KeysUpdateQueue SAuthorizationsVersion1 -> queueMapper PUELevel2KeysV1 _pLevel2KeysUpdateQueue SAuthorizationsVersion2 -> queueMapper PUELevel2KeysV2 _pLevel2KeysUpdateQueue + SAuthorizationsVersion3 -> queueMapper PUELevel2KeysV3 _pLevel2KeysUpdateQueue ) `merge` queueMapper PUEProtocol _pProtocolQueue `merge` queueMapperOptional PUEElectionDifficulty _pElectionDifficultyQueue @@ -936,6 +949,7 @@ getBlockPendingUpdates = liftSkovQueryStateBHI query `merge` queueMapperOptional PUEBlockEnergyLimit _pBlockEnergyLimitQueue `merge` queueMapperOptional PUEFinalizationCommitteeParameters _pFinalizationCommitteeParametersQueue `merge` queueMapperOptional PUEValidatorScoreParameters _pValidatorScoreParametersQueue + `merge` queueMapperConditional PUEMaxLockDuration _pMaxLockDurationQueue where cpv :: SChainParametersVersion cpv cpv = chainParametersVersion @@ -946,6 +960,10 @@ getBlockPendingUpdates = liftSkovQueryStateBHI query queueMapperOptional _ NoParam = [] queueMapperOptional constructor (SomeParam queue) = queueMapper constructor queue + queueMapperConditional :: (a -> PendingUpdateEffect) -> Conditionally b (UQ.UpdateQueue a) -> [(TransactionTime, PendingUpdateEffect)] + queueMapperConditional _ CFalse = [] + queueMapperConditional constructor (CTrue queue) = queueMapper constructor queue + -- Merge two ascending lists into an ascending list. merge :: [(TransactionTime, PendingUpdateEffect)] -> @@ -1018,6 +1036,9 @@ getNextUpdateSequenceNumbers = liftSkovQueryStateBHI query mNextSequenceNumber :: UQ.OUpdateQueue pt cpv e -> U.UpdateSequenceNumber mNextSequenceNumber NoParam = minUpdateSequenceNumber mNextSequenceNumber (SomeParam q) = UQ._uqNextSequenceNumber q + cNextSequenceNumber :: Conditionally b (UQ.UpdateQueue e) -> U.UpdateSequenceNumber + cNextSequenceNumber CFalse = minUpdateSequenceNumber + cNextSequenceNumber (CTrue q) = UQ._uqNextSequenceNumber q query bs = do updates <- BS.getUpdates bs let UQ.PendingUpdates{..} = UQ._pendingUpdates updates @@ -1044,7 +1065,8 @@ getNextUpdateSequenceNumbers = liftSkovQueryStateBHI query _nusnBlockEnergyLimit = mNextSequenceNumber _pBlockEnergyLimitQueue, _nusnFinalizationCommitteeParameters = mNextSequenceNumber _pFinalizationCommitteeParametersQueue, _nusnValidatorScoreParameters = mNextSequenceNumber _pValidatorScoreParametersQueue, - _nusnProtocolLevelTokensParameters = maybeConditionally minUpdateSequenceNumber id (UQ._pltUpdateSequenceNumber updates) + _nusnProtocolLevelTokensParameters = maybeConditionally minUpdateSequenceNumber id (UQ._pltUpdateSequenceNumber updates), + _nusnMaxLockDuration = cNextSequenceNumber _pMaxLockDurationQueue } -- | Get the index of accounts with scheduled releases. @@ -1181,7 +1203,7 @@ getAccountList = liftSkovQueryStateBHI BS.getAccountList -- | Get a list of protocol level tokens that exist in the block state. getTokenList :: BlockHashInput -> MVR finconf (BHIQueryResponse [TokenId]) -getTokenList = liftSkovQueryStateBHI BS.getPLTList +getTokenList = liftSkovQueryStateBHI queryPLTList -- | Get a list of all smart contract instances in the block state. getInstanceList :: BlockHashInput -> MVR finconf (BHIQueryResponse [ContractAddress]) @@ -1192,9 +1214,21 @@ getModuleList :: BlockHashInput -> MVR finconf (BHIQueryResponse [ModuleRef]) getModuleList = liftSkovQueryStateBHI BS.getModuleList -- | Get the details of a token in the block state. -getTokenInfo :: BlockHashInput -> TokenId -> MVR finconf (BHIQueryResponse (Either QueryTokenInfoError Tokens.TokenInfo)) +getTokenInfo :: BlockHashInput -> TokenId -> MVR finconf (BHIQueryResponse (Either QueryTokenModuleError Tokens.TokenInfo)) getTokenInfo blockHashInput tokenId = liftSkovQueryStateBHI (queryTokenInfo tokenId) blockHashInput +-- | Get the details of token authorizations in the block state. +getTokenAuthorizations :: BlockHashInput -> TokenId -> MVR finconf (BHIQueryResponse (Either QueryTokenModuleError Tokens.TokenAuthorizations)) +getTokenAuthorizations blockHashInput tokenId = liftSkovQueryStateBHI (queryTokenAuthorizations tokenId) blockHashInput + +-- | Get a list of all PLT lock ids that exist in the block state. +getLockList :: BlockHashInput -> MVR finconf (BHIQueryResponse [Locks.LockId]) +getLockList = liftSkovQueryStateBHI queryLockList + +-- | Get the CBOR-encoded `lock-info` payload for a given lock id. +getLockInfo :: BlockHashInput -> SerializedLockId -> MVR finconf (BHIQueryResponse (Either QueryLockError Locks.LockInfo)) +getLockInfo blockHashInput lockId = liftSkovQueryStateBHI (queryLockInfo lockId) blockHashInput + -- | Get the details of an account in the block state. -- The account can be given via an address, an account index or a credential registration id. -- In the latter case we lookup the account the credential is associated with, even if it was diff --git a/concordium-consensus/src/Concordium/Scheduler.hs b/concordium-consensus/src/Concordium/Scheduler.hs index caeb124aec..f998953d2a 100644 --- a/concordium-consensus/src/Concordium/Scheduler.hs +++ b/concordium-consensus/src/Concordium/Scheduler.hs @@ -5,6 +5,9 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} +-- We suppress redundant constraint warnings since GHC does not detect when a constraint is used +-- for pattern matching. (See: https://gitlab.haskell.org/ghc/ghc/-/issues/20896) +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- | -- The scheduler executes transactions (including credential deployment), updating the current block state. @@ -72,6 +75,7 @@ import qualified Concordium.GlobalState.BakerInfo as BI import Concordium.GlobalState.BlockState ( AccountAllowance (..), AccountOperations (..), + BlockStateOperations, ContractStateOperations (..), InstanceInfoType (..), InstanceInfoTypeV (..), @@ -104,7 +108,9 @@ import Lens.Micro.Platform import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens (PLTConfiguration (..)) +import Concordium.Scheduler.ProtocolLevelTokens.KernelImplementation (PLTExecutionError (..)) import qualified Concordium.Scheduler.ProtocolLevelTokens.Module as TokenModule +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler as RustScheduler import Concordium.Scheduler.WasmIntegration.V1 (ReceiveResultData (rrdCurrentState)) import Concordium.Types.Accounts import Concordium.Types.Option @@ -146,10 +152,10 @@ type CheckHeaderResult m = CheckHeaderResult' (IndexedAccount m) -- Returns the sender account and the cost to be charged for checking the header. checkHeader :: forall msg m. - (TransactionData msg, SchedulerMonad m) => + (TransactionData msg, BlockStateOperations m) => msg -> Maybe TVer.VerificationResult -> - ExceptT (Maybe FailureKind) m (CheckHeaderResult m) + ExceptT (Maybe FailureKind) (SchedulerT m) (CheckHeaderResult m) checkHeader meta mVerRes = do case sSupportsSponsoredTransactions (protocolVersion @(MPV m)) of SFalse @@ -281,7 +287,10 @@ checkTransactionVerificationResult (TVer.NotOk TVer.SponsoredTransactionMissingS -- * @Nothing@ if the transaction would exceed the remaining block energy. -- * @Just result@ if the transaction failed ('TxInvalid') or was successfully committed -- ('TxValid', with either 'TxSuccess' or 'TxReject'). -dispatch :: forall msg m. (TransactionData msg, SchedulerMonad m) => (msg, Maybe TVer.VerificationResult) -> m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) +dispatch :: + forall msg m. + (TransactionData msg, BlockStateOperations m) => + (msg, Maybe TVer.VerificationResult) -> SchedulerT m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) dispatch (msg, mVerRes) = do validMeta <- runExceptT (checkHeader msg mVerRes) case validMeta of @@ -310,12 +319,12 @@ dispatch (msg, mVerRes) = do -- ('TxValid', with either 'TxSuccess' or 'TxReject'). dispatchTransactionBody :: forall msg m res. - (TransactionData msg, SchedulerMonad m, TransactionResult res) => + (TransactionData msg, BlockStateOperations m, TransactionResult res) => -- | Transaction to execute. msg -> -- | Sender/sponsor account and header check energy cost. CheckHeaderResult m -> - m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) + SchedulerT m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) dispatchTransactionBody msg CheckHeaderResult{..} = do let meta = transactionHeader msg -- At this point the transaction is going to be committed to the block. @@ -369,6 +378,7 @@ dispatchTransactionBody msg CheckHeaderResult{..} = do -- NB: We already account for the cost we used here. _wtcCurrentlyUsedBlockEnergy = usedBlockEnergy + chrCheckHeaderCost, _wtcTransactionIndex = tsIndex, + _wtcTransactionSequenceNumber = thNonce meta, .. } -- Now pass the decoded payload to the respective transaction handler which contains @@ -447,8 +457,10 @@ dispatchTransactionBody msg CheckHeaderResult{..} = do onlyWithDelegation $ handleConfigureDelegation (mkWTC TTConfigureDelegation) cdCapital cdRestakeEarnings cdDelegationTarget TokenUpdate{..} -> - onlyWithPLT $ - handleTokenUpdate (mkWTC TTTokenUpdate) tuTokenId tuOperations + onlyWithPLT $ handleTokenUpdate (mkWTC TTTokenUpdate) tuTokenId tuOperations + MetaUpdate{..} -> + -- 'MetaUpdate' is only supported from P11, where we have 'PLTStateV1'. + onlyWithPLTV1 $ handleMetaUpdate (mkWTC TTMetaUpdate) muOperations where -- Function @onlyWithoutDelegation k@ fails if the protocol version @MPV m@ supports -- delegation. Otherwise, it continues with @k@, which may assume the chain parameters version @@ -475,17 +487,21 @@ dispatchTransactionBody msg CheckHeaderResult{..} = do onlyWithPLT c = case sSupportsPLT (accountVersion @(AccountVersionFor (MPV m))) of SFalse -> error "Operation unsupported at this protocol version." STrue -> c + onlyWithPLTV1 :: ((PltStateVersionFor (MPV m) ~ PLTStateV1) => a) -> a + onlyWithPLTV1 c = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateV1 -> c + _ -> error "Operation unsupported at this protocol version." cHasSponsorDetails = (sHasSponsorDetails (sTransactionOutcomesVersionFor (protocolVersion @(MPV m)))) handleTransferWithSchedule :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> AccountAddress -> [(Timestamp, Amount)] -> -- | Nothing in case of a TransferWithSchedule and Just in case of a TransferWithScheduleAndMemo Maybe Memo -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleTransferWithSchedule wtc twsTo twsSchedule maybeMemo = withDeposit wtc c k where senderAccount = wtc ^. wtcSenderAccount @@ -559,10 +575,10 @@ handleTransferWithSchedule wtc twsTo twsSchedule maybeMemo = withDeposit wtc c k return (TxSuccess eventList) handleTransferToPublic :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> SecToPubAmountTransferData -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleTransferToPublic wtc transferData@SecToPubAmountTransferData{..} = do cryptoParams <- TVer.getCryptographicParameters withDeposit wtc (c cryptoParams) k @@ -611,10 +627,10 @@ handleTransferToPublic wtc transferData@SecToPubAmountTransferData{..} = do ] handleTransferToEncrypted :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> Amount -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleTransferToEncrypted wtc toEncrypted = do cryptoParams <- TVer.getCryptographicParameters withDeposit wtc (c cryptoParams) k @@ -655,14 +671,14 @@ handleTransferToEncrypted wtc toEncrypted = do handleEncryptedAmountTransfer :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> -- | Receiver address. AccountAddress -> EncryptedAmountTransferData -> -- | Nothing in case of an EncryptedAmountTransfer and Just in case of an EncryptedAmountTransferWithMemo Maybe Memo -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleEncryptedAmountTransfer wtc toAddress transferData@EncryptedAmountTransferData{..} maybeMemo = do cryptoParams <- TVer.getCryptographicParameters withDeposit wtc (c cryptoParams) k @@ -746,11 +762,11 @@ handleEncryptedAmountTransfer wtc toAddress transferData@EncryptedAmountTransfer -- | Handle the deployment of a module. handleDeployModule :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> -- | The module to deploy. Wasm.WasmModule -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleDeployModule wtc mod = withDeposit wtc c k where @@ -833,7 +849,7 @@ getCurrentContractInstanceTicking' cref = do -- | Handle the initialization of a contract instance. handleInitContract :: forall m res. - (SchedulerMonad m, TransactionResult res) => + (BlockStateOperations m, TransactionResult res) => WithDepositContext m -> -- | The amount to initialize the contract instance with. Amount -> @@ -843,7 +859,7 @@ handleInitContract :: Wasm.InitName -> -- | Parameter expression to initialize with. Wasm.Parameter -> - m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) + SchedulerT m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) handleInitContract wtc initAmount modref initName param = withDeposit wtc c k where @@ -1005,7 +1021,7 @@ handleInitContract wtc initAmount modref initName param = ] handleSimpleTransfer :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> -- | Address to send the amount to, either account or contract. AccountAddress -> @@ -1013,7 +1029,7 @@ handleSimpleTransfer :: Amount -> -- | Nothing in case of a Transfer and Just in case of a TransferWithMemo Maybe Memo -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleSimpleTransfer wtc toAddr transferamount maybeMemo = withDeposit wtc c defaultSuccess where @@ -1036,7 +1052,7 @@ handleSimpleTransfer wtc toAddr transferamount maybeMemo = -- | Handle a top-level update transaction to a contract. handleUpdateContract :: - (SchedulerMonad m, TransactionResult res) => + (BlockStateOperations m, TransactionResult res) => WithDepositContext m -> -- | Amount to invoke the contract's receive method with. Amount -> @@ -1046,7 +1062,7 @@ handleUpdateContract :: Wasm.ReceiveName -> -- | Message to send to the receive method. Wasm.Parameter -> - m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) + SchedulerT m (Maybe (TransactionSummary' (TransactionOutcomesVersionFor (MPV m)) res)) handleUpdateContract wtc uAmount uAddress uReceiveName uMessage = withDeposit wtc computeAndCharge defaultSuccess where @@ -1299,18 +1315,11 @@ handleContractUpdateV1 depth originAddr istance checkAndGetSender transferAmount Nothing -> do -- In protocol version 4 we did not emit the interrupt event in this failing case. -- That was a mistake which is fixed in P5. - let newEvents = - case demoteProtocolVersion (protocolVersion @(MPV m)) of - P1 -> events - P2 -> events - P3 -> events - P4 -> events - P5 -> resumeEvent False : interruptEvent : events - P6 -> resumeEvent False : interruptEvent : events - P7 -> resumeEvent False : interruptEvent : events - P8 -> resumeEvent False : interruptEvent : events - P9 -> resumeEvent False : interruptEvent : events - P10 -> resumeEvent False : interruptEvent : events + let newEvents + | demoteProtocolVersion (protocolVersion @(MPV m)) < P5 = + events + | otherwise = + resumeEvent False : interruptEvent : events go newEvents =<< runInterpreter (return . WasmV1.resumeReceiveFun rrdInterruptedConfig rrdCurrentState False entryBalance (WasmV1.Error (WasmV1.EnvFailure (WasmV1.MissingContract imcTo))) Nothing) Just (InstanceInfoV0 targetInstance) -> do -- we are invoking a V0 instance. @@ -1999,7 +2008,7 @@ checkSignatureVerifyKeyProof = Proofs.checkDlog25519ProofBlock -- If the balance check has not been made, the behaviour is undefined. (Most likely, -- this will lead to an underflow and an invariant violation.) handleAddBaker :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, SchedulerMonad m) => + (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, BlockStateOperations m) => WithDepositContext m -> BakerElectionVerifyKey -> BakerSignVerifyKey -> @@ -2011,7 +2020,7 @@ handleAddBaker :: Amount -> -- | Whether to restake the baker's earnings Bool -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleAddBaker wtc abElectionVerifyKey abSignatureVerifyKey abAggregationVerifyKey abProofSig abProofElection abProofAggregation abBakingStake abRestakeEarnings = withDeposit wtc c k where @@ -2099,7 +2108,7 @@ checkConfigureBakerKeys senderAddress BakerKeysWithProofs{..} = handleConfigureBaker :: forall m. ( PVSupportsDelegation (MPV m), - SchedulerMonad m + BlockStateOperations m ) => WithDepositContext m -> -- | The equity capital of the baker @@ -2120,7 +2129,7 @@ handleConfigureBaker :: Maybe AmountFraction -> -- | Whether to suspend/resume the baker. Maybe Bool -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleConfigureBaker wtc cbCapital @@ -2304,12 +2313,12 @@ data ConfigureDelegationCont (av :: AccountVersion) -- | Handler for a configure delegation transaction. handleConfigureDelegation :: forall m. - (PVSupportsDelegation (MPV m), SchedulerMonad m) => + (PVSupportsDelegation (MPV m), BlockStateOperations m) => WithDepositContext m -> Maybe Amount -> Maybe Bool -> Maybe DelegationTarget -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleConfigureDelegation wtc cdCapital cdRestakeEarnings cdDelegationTarget = withDeposit wtc tickAndGetAccountBalance (const executeConfigure) where @@ -2415,9 +2424,9 @@ handleConfigureDelegation wtc cdCapital cdRestakeEarnings cdDelegationTarget = -- * If the account is the cool-down period for another baker change, the transaction fails ('BakerInCooldown'). -- * Otherwise, the baker is removed, which takes effect after the cool-down period. handleRemoveBaker :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, SchedulerMonad m) => + (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, BlockStateOperations m) => WithDepositContext m -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleRemoveBaker wtc = withDeposit wtc c k where @@ -2438,11 +2447,11 @@ handleRemoveBaker wtc = BI.BRChangePending _ -> return (TxReject BakerInCooldown) handleUpdateBakerStake :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, SchedulerMonad m) => + (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, BlockStateOperations m) => WithDepositContext m -> -- | new stake Amount -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleUpdateBakerStake wtc newStake = withDeposit wtc c k where @@ -2474,11 +2483,11 @@ handleUpdateBakerStake wtc newStake = return (TxReject StakeUnderMinimumThresholdForBaking) handleUpdateBakerRestakeEarnings :: - (AccountVersionFor (MPV m) ~ 'AccountV0, SchedulerMonad m) => + (AccountVersionFor (MPV m) ~ 'AccountV0, BlockStateOperations m) => WithDepositContext m -> -- | Whether to restake earnings Bool -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleUpdateBakerRestakeEarnings wtc newRestakeEarnings = withDeposit wtc c k where senderAccount = wtc ^. wtcSenderAccount @@ -2507,7 +2516,7 @@ handleUpdateBakerRestakeEarnings wtc newRestakeEarnings = withDeposit wtc c k -- If the balance check has not been made, the behaviour is undefined. (Most likely, -- this will lead to an underflow and an invariant violation.) handleUpdateBakerKeys :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, SchedulerMonad m) => + (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, BlockStateOperations m) => WithDepositContext m -> BakerElectionVerifyKey -> BakerSignVerifyKey -> @@ -2515,7 +2524,7 @@ handleUpdateBakerKeys :: Proofs.Dlog25519Proof -> Proofs.Dlog25519Proof -> BakerAggregationProof -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleUpdateBakerKeys wtc bkuElectionKey bkuSignKey bkuAggregationKey bkuProofSig bkuProofElection bkuProofAggregation = withDeposit wtc c k where @@ -2569,11 +2578,11 @@ handleUpdateBakerKeys wtc bkuElectionKey bkuSignKey bkuAggregationKey bkuProofSi -- Note that the function only fails with `TxInvalid` and thus failed transactions are not committed to chain. handleDeployCredential :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m) => -- | Credentials to deploy with the current verification status. TVer.CredentialDeploymentWithStatus -> TransactionHash -> - m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) handleDeployCredential (WithMetadata{wmdData = cred@AccountCreation{messageExpiry = messageExpiry, credential = cdi}}, mVerRes) cdiHash = do res <- runExceptT $ do cm <- lift getChainMetadata @@ -2638,7 +2647,7 @@ handleDeployCredential (WithMetadata{wmdData = cred@AccountCreation{messageExpir -- | Updates the credential keys in the credential with the given Credential ID. -- It rejects if there is no credential with the given Credential ID. handleUpdateCredentialKeys :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> -- | Registration ID of the credential we are updating. ID.CredentialRegistrationID -> @@ -2646,7 +2655,7 @@ handleUpdateCredentialKeys :: ID.CredentialPublicKeys -> -- | Signatures on the transaction. This is needed to check that a specific credential signed. TransactionSignature -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleUpdateCredentialKeys wtc cid keys sigs = withDeposit wtc c k where @@ -2677,15 +2686,31 @@ handleUpdateCredentialKeys wtc cid keys sigs = handleTokenUpdate :: forall m. ( PVSupportsPLT (MPV m), - SchedulerMonad m + BlockStateOperations m + ) => + WithDepositContext m -> + -- | Token symbol identifying the token to receive the operations. + TokenId -> + -- | Operations for the token. + RawCbor -> + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) +handleTokenUpdate depositContext tokenId tokenOperations = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateV0 -> handleTokenUpdateHaskellManaged depositContext tokenId tokenOperations + SPLTStateV1 -> RustScheduler.executeTransaction depositContext (TokenUpdate tokenId tokenOperations) + +-- | Handler for a token update transaction, for protocol version where PLT state is managed in Haskell. +handleTokenUpdateHaskellManaged :: + forall m. + ( PVSupportsHaskellManagedPLT (MPV m), + BlockStateOperations m ) => WithDepositContext m -> -- | Token symbol identifying the token to receive the operations. TokenId -> -- | Operations for the token. - TokenParameter -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) -handleTokenUpdate depositContext tokenId tokenOperations = + RawCbor -> + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) +handleTokenUpdateHaskellManaged depositContext tokenId tokenOperations = withDeposit depositContext computeTransaction commitTransaction where senderAccount = depositContext ^. wtcSenderAccount @@ -2721,8 +2746,8 @@ handleTokenUpdate depositContext tokenId tokenOperations = TokenModuleRef -> Token.TokenIndex -> IndexedAccount m -> - TokenParameter -> - m (Either (PLTExecutionError PLTTypes.EncodedTokenRejectReason) [Event], Energy) + RawCbor -> + SchedulerT m (Either (PLTExecutionError PLTTypes.EncodedTokenRejectReason) [Event], Energy) invokeTokenOperations energy _ tokenIndex sender parameter = do withBlockStateRollback $ do let tc = @@ -2733,14 +2758,27 @@ handleTokenUpdate depositContext tokenId tokenOperations = (res, events, energyUsed) <- runPLTWithEnergy tokenIndex energy $ TokenModule.executeTokenUpdateTransaction tc parameter return ((events <$ res, energyUsed), isLeft res) +-- | Handler for a meta update transaction. +handleMetaUpdate :: + forall m. + ( PltStateVersionFor (MPV m) ~ PLTStateV1, + BlockStateOperations m + ) => + WithDepositContext m -> + -- | Operations. + RawCbor -> + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) +handleMetaUpdate depositContext tokenOperations = + RustScheduler.executeTransaction depositContext (MetaUpdate tokenOperations) + -- * Chain updates -- | Handle a chain update message handleChainUpdate :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m) => TVer.ChainUpdateWithStatus -> - m (TxResult (TransactionOutcomesVersionFor (MPV m))) + SchedulerT m (TxResult (TransactionOutcomesVersionFor (MPV m))) handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVerificationResult) = do cm <- getChainMetadata -- check that payload si @@ -2804,6 +2842,9 @@ handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVe RootUpdatePayload (Level2KeysRootUpdateV2 u) -> case sauv of SAuthorizationsVersion2 -> checkSigAndEnqueue $ UVLevel2Keys u _ -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion + RootUpdatePayload (Level2KeysRootUpdateV3 u) -> case sauv of + SAuthorizationsVersion3 -> checkSigAndEnqueue $ UVLevel2Keys u + _ -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion Level1UpdatePayload (Level1KeysLevel1Update u) -> checkSigAndEnqueue $ UVLevel1Keys u Level1UpdatePayload (Level2KeysLevel1Update u) -> case sauv of SAuthorizationsVersion0 -> checkSigAndEnqueue $ UVLevel2Keys u @@ -2814,6 +2855,9 @@ handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVe Level1UpdatePayload (Level2KeysLevel1UpdateV2 u) -> case sauv of SAuthorizationsVersion2 -> checkSigAndEnqueue $ UVLevel2Keys u _ -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion + Level1UpdatePayload (Level2KeysLevel1UpdateV3 u) -> case sauv of + SAuthorizationsVersion3 -> checkSigAndEnqueue $ UVLevel2Keys u + _ -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion TimeoutParametersUpdatePayload u -> case sIsSupported SPTTimeoutParameters scpv of STrue -> checkSigAndEnqueue $ UVTimeoutParameters u SFalse -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion @@ -2839,12 +2883,15 @@ handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVe handleCreatePLT uiHeader payload >>= \case Left invalidReason -> return $ TxInvalid invalidReason Right valid -> buildValidTxSummary' valid + MaxLockDurationUpdatePayload u -> case sSupportsTokenParameters sauv of + STrue -> checkSigAndEnqueue $ UVMaxLockDuration u + SFalse -> return $ TxInvalid NotSupportedAtCurrentProtocolVersion where scpv :: SChainParametersVersion (ChainParametersVersionFor (MPV m)) scpv = chainParametersVersion sauv :: SAuthorizationsVersion (AuthorizationsVersionFor (MPV m)) sauv = sAuthorizationsVersionFor $ protocolVersion @(MPV m) - checkSigThen :: m (TxResult tov) -> m (TxResult tov) + checkSigThen :: SchedulerT m (TxResult tov) -> SchedulerT m (TxResult tov) checkSigThen cont = do case maybeVerificationResult of Just (TVer.Ok (TVer.ChainUpdateSuccess keysHash _)) -> do @@ -2866,7 +2913,7 @@ handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVe case checkTransactionVerificationResult newVerRes of Left failure -> return $ TxInvalid failure Right _ -> cont - checkSigAndEnqueue :: UpdateValue (ChainParametersVersionFor (MPV m)) (AuthorizationsVersionFor (MPV m)) -> m (TxResult (TransactionOutcomesVersionFor (MPV m))) + checkSigAndEnqueue :: UpdateValue (ChainParametersVersionFor (MPV m)) (AuthorizationsVersionFor (MPV m)) -> SchedulerT m (TxResult (TransactionOutcomesVersionFor (MPV m))) checkSigAndEnqueue = checkSigThen . enqueue enqueue change = do enqueueUpdate (updateEffectiveTime uiHeader) change @@ -2895,8 +2942,24 @@ handleChainUpdate (WithMetadata{wmdData = ui@UpdateInstruction{..}, ..}, maybeVe -- -- Unlike the other chain updates there is no support for queuing the update and the effective time -- is required to be zero. -handleCreatePLT :: (SchedulerMonad m, PVSupportsPLT (MPV m)) => UpdateHeader -> CreatePLT -> m (Either FailureKind ValidResult) -handleCreatePLT updateHeader payload = runExceptT $ do +handleCreatePLT :: + forall m. + (BlockStateOperations m, PVSupportsPLT (MPV m)) => + UpdateHeader -> CreatePLT -> SchedulerT m (Either FailureKind ValidResult) +handleCreatePLT updateHeader payload = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateV0 -> + handleCreatePLTHaskellManaged updateHeader payload + SPLTStateV1 -> RustScheduler.executeChainUpdate updateHeader payload + +-- | Handler for processing chain update creating a new protocol level token, for protocol version where PLT state is managed in Haskell. +-- It is assumed that the signatures have already been checked. +-- +-- Unlike the other chain updates there is no support for queuing the update and the effective time +-- is required to be zero. +handleCreatePLTHaskellManaged :: + (BlockStateOperations m, PVSupportsHaskellManagedPLT (MPV m)) => + UpdateHeader -> CreatePLT -> SchedulerT m (Either FailureKind ValidResult) +handleCreatePLTHaskellManaged updateHeader payload = runExceptT $ do unless (updateEffectiveTime updateHeader == 0) $ throwError InvalidUpdateTime let tokenId = payload ^. cpltTokenId maybeExistingToken <- lift $ getTokenIndex tokenId @@ -2920,12 +2983,12 @@ handleCreatePLT updateHeader payload = runExceptT $ do return $ TxSuccess events handleUpdateCredentials :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> OrdMap.Map ID.CredentialIndex ID.CredentialDeploymentInformation -> [ID.CredentialRegistrationID] -> ID.AccountThreshold -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleUpdateCredentials wtc cdis removeRegIds threshold = withDeposit wtc c k where @@ -3044,11 +3107,11 @@ handleUpdateCredentials wtc cdis removeRegIds threshold = -- | Charges energy based on payload size and emits a 'DataRegistered' event. handleRegisterData :: - (SchedulerMonad m) => + (BlockStateOperations m) => WithDepositContext m -> -- | The data to register. RegisteredData -> - m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) + SchedulerT m (Maybe (TransactionSummary (TransactionOutcomesVersionFor (MPV m)))) handleRegisterData wtc regData = withDeposit wtc c defaultSuccess where @@ -3126,7 +3189,7 @@ handleRegisterData wtc regData = -- and `ftUnprocessedCredentials`. filterTransactions :: forall m. - (SchedulerMonad m, TimeMonad m) => + (BlockStateOperations m, MonadLogger m, TimeMonad m, MonadProtocolVersion m) => -- | Maximum block size in bytes. Integer -> -- | Timeout for block construction. @@ -3134,7 +3197,7 @@ filterTransactions :: UTCTime -> -- | Transactions to make a block out of. [TransactionGroup] -> - m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) + SchedulerT m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) filterTransactions maxSize timeout groups0 = do maxEnergy <- getMaxBlockEnergy credLimit <- getAccountCreationLimit @@ -3161,7 +3224,7 @@ filterTransactions maxSize timeout groups0 = do Bool -> -- \^Whether or not the block timeout is reached FilteredTransactions (TransactionOutcomesVersionFor (MPV m)) -> -- \^Currently accumulated result [TransactionGroup] -> -- \^Grouped transactions to process - m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) + SchedulerT m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) -- All block items are processed. We accumulate the added items -- in reverse order, so reverse the list before returning. runNext _ _ _ _ fts [] = return fts{ftAdded = reverse (ftAdded fts)} @@ -3233,7 +3296,7 @@ filterTransactions maxSize timeout groups0 = do in runNext maxEnergy currentSize credLimit False newFts groups -- Run a single credential and continue with 'runNext'. - runCredential :: TVer.CredentialDeploymentWithStatus -> m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) + runCredential :: TVer.CredentialDeploymentWithStatus -> SchedulerT m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) runCredential cws@(c@WithMetadata{..}, verRes) = do totalEnergyUsed <- getUsedEnergy let csize = size + fromIntegral wmdSize @@ -3273,7 +3336,7 @@ filterTransactions maxSize timeout groups0 = do Integer -> -- \^Current size of transactions in the block. FilteredTransactions (TransactionOutcomesVersionFor (MPV m)) -> [TVer.TransactionWithStatus] -> -- \^Current group to process. - m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) + SchedulerT m (FilteredTransactions (TransactionOutcomesVersionFor (MPV m))) runTransactionGroup currentSize currentFts (t : ts) = do totalEnergyUsed <- getUsedEnergy let csize = currentSize + fromIntegral (transactionSize (fst t)) @@ -3375,9 +3438,9 @@ filterTransactions maxSize timeout groups0 = do -- * @Right outcomes@ if all transactions are successful, with the given outcomes. runTransactions :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m, MonadLogger m) => [TVer.BlockItemWithStatus] -> - m (Either (Maybe FailureKind) [(BlockItem, TransactionSummary (TransactionOutcomesVersionFor (MPV m)))]) + SchedulerT m (Either (Maybe FailureKind) [(BlockItem, TransactionSummary (TransactionOutcomesVersionFor (MPV m)))]) runTransactions = go [] where go valid (bi : ts) = @@ -3391,7 +3454,9 @@ runTransactions = go [] Nothing -> return (Left Nothing) go valid [] = return (Right (reverse $ map (\(x, y) -> (fst x, y)) valid)) - predispatch :: TVer.BlockItemWithStatus -> m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) + predispatch :: + TVer.BlockItemWithStatus -> + SchedulerT m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) predispatch (WithMetadata{wmdData = NormalTransaction tr, ..}, verRes) = dispatch (WithMetadata{wmdData = tr, ..}, verRes) predispatch (WithMetadata{wmdData = CredentialDeployment cred, ..}, verRes) = handleDeployCredential (WithMetadata{wmdData = cred, ..}, verRes) wmdHash predispatch (WithMetadata{wmdData = ChainUpdate cu, ..}, verRes) = Just <$> handleChainUpdate (WithMetadata{wmdData = cu, ..}, verRes) @@ -3409,9 +3474,9 @@ runTransactions = go [] -- of results. execTransactions :: forall m. - (SchedulerMonad m) => + (BlockStateOperations m, MonadLogger m) => [TVer.BlockItemWithStatus] -> - m (Either (Maybe FailureKind) ()) + SchedulerT m (Either (Maybe FailureKind) ()) execTransactions = go where -- Same implementation as 'runTransactions', just that valid block items @@ -3427,7 +3492,7 @@ execTransactions = go return (Left (Just reason)) go [] = return (Right ()) - predispatch :: TVer.BlockItemWithStatus -> m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) + predispatch :: TVer.BlockItemWithStatus -> SchedulerT m (Maybe (TxResult (TransactionOutcomesVersionFor (MPV m)))) predispatch (WithMetadata{wmdData = NormalTransaction tr, ..}, verRes) = dispatch (WithMetadata{wmdData = tr, ..}, verRes) predispatch (WithMetadata{wmdData = CredentialDeployment cred, ..}, verRes) = handleDeployCredential (WithMetadata{wmdData = cred, ..}, verRes) wmdHash predispatch (WithMetadata{wmdData = ChainUpdate cu, ..}, verRes) = Just <$> handleChainUpdate (WithMetadata{wmdData = cu, ..}, verRes) diff --git a/concordium-consensus/src/Concordium/Scheduler/Environment.hs b/concordium-consensus/src/Concordium/Scheduler/Environment.hs index af518df4af..56c4c5e3fe 100644 --- a/concordium-consensus/src/Concordium/Scheduler/Environment.hs +++ b/concordium-consensus/src/Concordium/Scheduler/Environment.hs @@ -11,15 +11,21 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} +-- We suppress redundant constraint warnings since GHC does not detect when a constraint is used +-- for pattern matching. (See: https://gitlab.haskell.org/ghc/ghc/-/issues/20896) +{-# OPTIONS_GHC -Wno-redundant-constraints #-} module Concordium.Scheduler.Environment where +import Data.Either import Data.Foldable import qualified Data.HashMap.Strict as HMap import qualified Data.HashSet as HSet +import qualified Data.Kind as DK import qualified Data.Map as Map import qualified Data.Set as Set +import Control.Monad import Control.Monad.Cont hiding (cont) import Control.Monad.RWS.Strict import Control.Monad.Trans.Reader (ReaderT (..), runReaderT) @@ -28,9 +34,9 @@ import Lens.Micro.Platform import qualified Concordium.Cost as Cost import Concordium.Crypto.EncryptedTransfers -import Concordium.GlobalState.Account (AccountUpdate (..), EncryptedAmountUpdate (..), auAmount, auEncrypted, auReleaseSchedule, emptyAccountUpdate) +import Concordium.GlobalState.Account (AccountUpdate (..), EncryptedAmountUpdate (..), auAmount, auEncrypted, auNonce, auReleaseSchedule, emptyAccountUpdate) import Concordium.GlobalState.BakerInfo -import Concordium.GlobalState.BlockState (AccountOperations (..), ContractStateOperations (..), InstanceInfo, InstanceInfoType (..), InstanceInfoTypeV (iiParameters, iiState), ModuleQuery (..), NewInstanceData, UpdatableContractState, iiBalance) +import Concordium.GlobalState.BlockState (AccountOperations (..), ContractStateOperations (..), InstanceInfo, InstanceInfoType (..), InstanceInfoTypeV (iiParameters, iiState), NewInstanceData, UpdatableContractState, iiBalance) import Concordium.GlobalState.Classes (MGSTrans (..)) import Concordium.GlobalState.Types import qualified Concordium.GlobalState.Wasm as GSWasm @@ -43,15 +49,93 @@ import qualified Concordium.TransactionVerification as TVer import Control.Exception (assert) +import qualified Concordium.GlobalState.BlockState as BS import qualified Concordium.GlobalState.ContractStateV1 as StateV1 import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens as Token import qualified Concordium.ID.Types as ID import Concordium.Scheduler.ProtocolLevelTokens.Kernel (PLTAccount, PLTKernelChargeEnergy, PLTKernelFail, PLTKernelPrivilegedUpdate) +import Concordium.Scheduler.ProtocolLevelTokens.KernelImplementation import qualified Concordium.Scheduler.WasmIntegration.V1 as V1 +import Concordium.TimeMonad import Concordium.Wasm (IsWasmVersion) import qualified Concordium.Wasm as GSWasm import Data.Proxy +-- | Context for executing a scheduler computation. +data ContextState = ContextState + { -- | Chain metadata + _chainMetadata :: !ChainMetadata, + -- | Maximum allowed block energy. + _maxBlockEnergy :: !Energy, + -- | Maximum number of accounts to be created in the same block. + _accountCreationLimit :: !CredentialsPerBlockLimit + } + +makeLenses ''ContextState + +-- | State accumulated during execution of a scheduler computation. +data SchedulerState (m :: DK.Type -> DK.Type) = SchedulerState + { -- | Current block state. + _ssBlockState :: !(UpdatableBlockState m), + -- | Energy used so far. + _ssEnergyUsed :: !Energy, + -- | The total execution costs so far. + _ssExecutionCosts :: !Amount, + -- | The next available transaction index. + _ssNextIndex :: !TransactionIndex + } + +makeLenses ''SchedulerState + +-- | Create an initial state for running a scheduler computation. +makeInitialSchedulerState :: UpdatableBlockState m -> SchedulerState m +makeInitialSchedulerState _ssBlockState = + SchedulerState + { _ssEnergyUsed = 0, + _ssExecutionCosts = 0, + _ssNextIndex = 0, + .. + } + +-- | Alias for the internal type used in @SchedulerT@. +type InternalSchedulerT m = RWST ContextState () (SchedulerState m) + +-- | Scheduler monad transformer. Extends a monad with the ability to execute scheduler computations. +-- Use @runSchedulerT@ to run the computation. +newtype SchedulerT (m :: DK.Type -> DK.Type) (a :: DK.Type) = SchedulerT + { _runSchedulerT :: InternalSchedulerT m m a + } + deriving + ( Functor, + Applicative, + Monad, + MonadState (SchedulerState m), + MonadReader ContextState, + MonadLogger, + TimeMonad + ) + +-- | Execute the computation using the provided context and scheduler state. +-- The return value is the value produced by the computation and the updated state of the scheduler. +runSchedulerT :: + (Monad m) => + SchedulerT m a -> + ContextState -> + SchedulerState m -> + m (a, SchedulerState m) +runSchedulerT computation contextState initialState = do + (value, resultingState, ()) <- runRWST (_runSchedulerT computation) contextState initialState + return (value, resultingState) + +instance MonadTrans SchedulerT where + {-# INLINE lift #-} + lift = SchedulerT . lift + +deriving via + (MGSTrans (InternalSchedulerT m) m) + instance + BlockStateTypes (SchedulerT m) + -- | An account index together with the canonical address. Sometimes it is -- difficult to pass an IndexedAccount and we only need the addresses. That is -- when this type is useful. @@ -85,6 +169,94 @@ class (Monad m) => StaticInformation m where -- | Get the current exchange rates, that is the Euro per NRG, micro CCD per Euro and the energy rate. getExchangeRates :: m ExchangeRates +instance (BS.BlockStateOperations m) => StaticInformation (SchedulerT m) where + {-# INLINE getMaxBlockEnergy #-} + getMaxBlockEnergy = view maxBlockEnergy + + {-# INLINE getChainMetadata #-} + getChainMetadata = view chainMetadata + + {-# INLINE getModuleInterfaces #-} + getModuleInterfaces mref = do + s <- use ssBlockState + lift (BS.bsoGetModule s mref) + + {-# INLINE getAccountCreationLimit #-} + getAccountCreationLimit = view accountCreationLimit + + {-# INLINE getContractInstance #-} + getContractInstance addr = lift . flip BS.bsoGetInstance addr =<< use ssBlockState + + {-# INLINE getStateAccount #-} + getStateAccount !addr = lift . flip BS.bsoGetAccount addr =<< use ssBlockState + + {-# INLINE getExchangeRates #-} + getExchangeRates = lift . BS.bsoGetExchangeRates =<< use ssBlockState + +deriving via + (MGSTrans (InternalSchedulerT m) m) + instance + (MonadProtocolVersion m) => MonadProtocolVersion (SchedulerT m) + +deriving via + (MGSTrans (InternalSchedulerT m) m) + instance + (BS.AccountOperations m) => BS.AccountOperations (SchedulerT m) + +deriving via + (MGSTrans (InternalSchedulerT m) m) + instance + (BS.ContractStateOperations m) => BS.ContractStateOperations (SchedulerT m) + +deriving via + (MGSTrans (InternalSchedulerT m) m) + instance + (BS.ModuleQuery m) => BS.ModuleQuery (SchedulerT m) + +instance + (BS.BlockStateOperations m, MonadProtocolVersion m) => + TVer.TransactionVerifier (SchedulerT m) + where + {-# INLINE registrationIdExists #-} + registrationIdExists !regid = + lift . flip BS.bsoRegIdExists regid =<< use ssBlockState + {-# INLINE getIdentityProvider #-} + getIdentityProvider !ipId = do + s <- use ssBlockState + lift (BS.bsoGetIdentityProvider s ipId) + {-# INLINE getAnonymityRevokers #-} + getAnonymityRevokers !arIds = do + s <- use ssBlockState + lift (BS.bsoGetAnonymityRevokers s arIds) + {-# INLINE getCryptographicParameters #-} + getCryptographicParameters = lift . BS.bsoGetCryptoParams =<< use ssBlockState + {-# INLINE getAccount #-} + getAccount !aaddr = do + s <- use ssBlockState + lift (fmap snd <$> BS.bsoGetAccount s aaddr) + {-# INLINE getNextUpdateSequenceNumber #-} + getNextUpdateSequenceNumber uType = lift . flip BS.bsoGetNextUpdateSequenceNumber uType =<< use ssBlockState + {-# INLINE getUpdateKeysCollection #-} + getUpdateKeysCollection = lift . BS.bsoGetUpdateKeyCollection =<< use ssBlockState + {-# INLINE getAccountAvailableAmount #-} + getAccountAvailableAmount = lift . BS.getAccountAvailableAmount + {-# INLINE getNextAccountNonce #-} + getNextAccountNonce = lift . BS.getAccountNonce + {-# INLINE getAccountVerificationKeys #-} + getAccountVerificationKeys = lift . BS.getAccountVerificationKeys + {-# INLINE energyToCcd #-} + energyToCcd v = do + s <- use ssBlockState + rate <- lift $ _erEnergyRate <$> BS.bsoGetExchangeRates s + return (computeCost rate v) + {-# INLINE getMaxBlockEnergy #-} + getMaxBlockEnergy = do + ctx <- ask + let maxEnergy = ctx ^. maxBlockEnergy + return maxEnergy + {-# INLINE checkExactNonce #-} + checkExactNonce = pure True + -- | When adding a validator or delegator to an account, this indicates whether the account has -- an existing delegator or validator that must be removed. data RemoveExistingStake @@ -94,343 +266,735 @@ data RemoveExistingStake NoExistingStake deriving (Eq, Show) --- | PLT module execution error. -data PLTExecutionError fail - = -- | The PLT module run out of energy during execution. - PLTEOutOfEnergy - | -- | The PLT module encountered a runtime error during execution. - PLTEFail fail - --- | Information needed to execute transactions in the form that is easy to use. -class - (Monad m, StaticInformation m, AccountOperations m, ContractStateOperations m, ModuleQuery m, MonadLogger m, MonadProtocolVersion m, TVer.TransactionVerifier m) => - SchedulerMonad m - where - -- | Get the 'AccountIndex' for an account, if it exists. - getAccountIndex :: AccountAddress -> m (Maybe AccountIndex) - - -- | Check whether the given account address would clash with any existing - -- account's address. The behaviour of this will generally depend on the - -- protocol version. - addressWouldClash :: AccountAddress -> m Bool - - -- | Commit to global state all the updates to local state that have - -- accumulated through the execution. This method is also in charge of - -- recording which accounts were affected by the transaction for reward and - -- other purposes. - -- Precondition: Each account affected in the change set must exist in the - -- block state. - commitChanges :: ChangeSet m -> m () - - -- | Commit a module interface and module value to global state. Returns @True@ - -- if this was successful, and @False@ if a module with the given Hash already - -- existed. Also store the code of the module for archival purposes. - commitModule :: (IsWasmVersion v) => (GSWasm.ModuleInterfaceV v, Wasm.WasmModuleV v) -> m Bool - - -- | Create new instance in the global state. - -- The instance is parametrised by the address, and the return value is the - -- address assigned to the new instance. - putNewInstance :: (IsWasmVersion v) => NewInstanceData (InstrumentedModuleRef m v) v -> m ContractAddress - - -- | Bump the next available transaction nonce of the account. - -- Precondition: the account exists in the block state. - increaseAccountNonce :: IndexedAccount m -> m () - - -- FIXME: This method should not be here, but rather in the transaction monad. - - -- | Update account credentials. - -- Preconditions: - -- - The account exists in the block state. - -- - The account threshold is reasonable. - updateAccountCredentials :: - AccountIndex -> - -- | The indices of credentials to remove from the account. - [ID.CredentialIndex] -> - -- | The new credentials to add. - Map.Map ID.CredentialIndex ID.AccountCredential -> - -- | The new account threshold - ID.AccountThreshold -> - m () - - -- | Create and add an empty account with the given public key, address and credential. - -- If an account with the given address already exists, @Nothing@ is returned. - -- Otherwise, the new account is returned, and the credential is added to the known credentials. - -- - -- It is not checked if the account's credential is a duplicate. - createAccount :: CryptographicParameters -> AccountAddress -> ID.AccountCredential -> m (Maybe (Account m)) - - -- | Notify energy used by the current execution. - -- Add to the current running total of energy used. - markEnergyUsed :: Energy -> m () - - -- | Get the currently used amount of block energy. - getUsedEnergy :: m Energy - - getRemainingEnergy :: m Energy - getRemainingEnergy = do - maxEnergy <- getMaxBlockEnergy - usedEnergy <- getUsedEnergy - return $! if usedEnergy <= maxEnergy then maxEnergy - usedEnergy else 0 - - -- | Get the next transaction index in the block, and increase the internal counter - bumpTransactionIndex :: m TransactionIndex - - -- | Record that the amount was charged for execution. Amount is distributed - -- at the end of block execution in accordance with the tokenomics principles. - notifyExecutionCost :: Amount -> m () - - -- | Notify the state that an amount has been transferred from public to - -- encrypted or vice-versa. - notifyEncryptedBalanceChange :: AmountDelta -> m () - - -- | Convert the given energy amount into an amount of GTU. The exchange - -- rate can vary depending on the current state of the blockchain. - energyToGtu :: Energy -> m Amount - - -- * Operations related to bakers. - - -- | Register this account as a baker. - -- The following results are possible: - -- - -- * @BASuccess id@: the baker was created with the specified 'BakerId'. - -- @id@ is always chosen to be the account index. - -- - -- * @BAInvalidAccount@: the address does not resolve to a valid account. - -- - -- * @BAAlreadyBaker@: the account is already registered as a baker. - -- - -- * @BAInsufficientBalance@: the balance on the account is insufficient to - -- stake the specified amount. - -- - -- * @BADuplicateAggregationKey@: the aggregation key is already in use. - -- - -- Note that if two results could apply, the first in this list takes precedence. - addBaker :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0) => - AccountIndex -> - BakerAdd -> - m BakerAddResult - - -- | From chain parameters version 1, this operation adds a validator on an account. - -- For details of the behaviour and return values, see - -- 'Concordium.GlobalState.BlockState.bsoAddValidator'. - -- - -- PRECONDITION: - -- * The account must exist; - -- * The account must not already be a validator; - -- * The flag must indicate if the account is currently a delegator, which will be removed; - -- * The account must have sufficient balance to cover the stake. - addValidator :: - (PVSupportsDelegation (MPV m)) => - AccountIndex -> - -- | Whether the account already has a delegator, which will be removed in the process. - RemoveExistingStake -> - ValidatorAdd -> - m (Either ValidatorConfigureFailure ()) - - -- | From chain parameters version 1, this operation updates or removes a validator on an - -- account. For details of the behaviour and return values, see - -- 'Concordium.GlobalState.BlockState.bsoUpdateValidator'. - -- - -- PRECONDITION: - -- * The account must exist; - -- * The account must be a validator; - -- * The account must have sufficient balance to cover the new stake. - updateValidator :: - (PVSupportsDelegation (MPV m)) => - Timestamp -> - AccountIndex -> - ValidatorUpdate -> - m (Either ValidatorConfigureFailure [BakerConfigureUpdateChange]) - - -- | From chain parameters version 1, this operation adds a delegator on an account. - -- For details of the behaviour and return values, see - -- 'Concordium.GlobalState.BlockState.bsoAddDelegator'. - -- - -- PRECONDITION: - -- * The account must exist; - -- * The account must not already be a delegator; - -- * The flag must indicate if the account is currently a validator, which will be removed; - -- * The account must have sufficient balance to cover the stake. - addDelegator :: - (PVSupportsDelegation (MPV m)) => - AccountIndex -> - -- | Whether the account already has a validator, which will be removed in the process. - RemoveExistingStake -> - DelegatorAdd -> - m (Either DelegatorConfigureFailure ()) - - -- | From chain parameters version 1, this operation updates or removes a delegator on an - -- account. For details of the behaviour and return values, see - -- 'Concordium.GlobalState.BlockState.bsoUpdateDelegator'. - -- - -- PRECONDITION: - -- * The account must exist; - -- * The account must be a delegator; - -- * The account must have sufficient balance to cover the new stake. - updateDelegator :: - (PVSupportsDelegation (MPV m)) => - Timestamp -> - AccountIndex -> - DelegatorUpdate -> - m (Either DelegatorConfigureFailure [DelegationConfigureUpdateChange]) - - -- | Remove the baker associated with an account. - -- The removal takes effect after a cooling-off period. - -- Removal may fail if the baker is already cooling-off from another change (e.g. stake reduction). - -- - -- The following results are possible: - -- - -- * @BRRemoved e@: the baker was removed, and will be in cooling-off until epoch @e@. - -- The change will take effect in epoch @e+1@. - -- - -- * @BRInvalidBaker@: the account address is not valid, or the account is not a baker. - -- - -- * @BRChangePending@: the baker is currently in a cooling-off period and so cannot be removed. - removeBaker :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0) => - AccountIndex -> - m BakerRemoveResult - - -- | Update the keys associated with an account. - -- It is assumed that the keys have already been checked for validity/ownership as - -- far as is necessary. - -- The only check on the keys is that the aggregation key is not a duplicate. - -- - -- The following results are possible: - -- - -- * @BKUSuccess@: the keys were updated - -- - -- * @BKUInvalidBaker@: the account does not exist or is not currently a baker. - -- - -- * @BKUDuplicateAggregationKey@: the aggregation key is a duplicate. - updateBakerKeys :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0) => - AccountIndex -> - BakerKeyUpdate -> - m BakerKeyUpdateResult - - -- | Update the stake associated with an account. - -- A reduction in stake will be delayed by the current cool-off period. - -- A change will not be made if there is already a cooling-off change - -- pending for the baker. - -- - -- The following results are possible: - -- - -- * @BSUStakeIncreased@: the baker's stake was increased. - -- This will take effect in the epoch after next. - -- - -- * @BSUStakeReduced e@: the baker's stake was reduced. - -- This will cool-off until epoch @e@ and take effect in epoch @e+1@. - -- - -- * @BSUStakeUnchanged@: there is no change to the baker's stake, but this update was successful. - -- - -- * @BSUInvalidBaker@: the account does not exist, or is not currently a baker. - -- - -- * @BSUChangePending@: the change could not be made since the account is already in a cooling-off period. - -- - -- * @BSUInsufficientBalance@: the account does not have sufficient balance to cover the staked amount. - updateBakerStake :: - (AccountVersionFor (MPV m) ~ 'AccountV0, ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0) => - AccountIndex -> - Amount -> - m BakerStakeUpdateResult - - -- | Update whether the baker automatically restakes the rewards it earns. - -- - -- The following results are possible: - -- - -- * @BREUUpdated id@: the flag was updated. - -- - -- * @BREUInvalidBaker@: the account does not exists, or is not currently a baker. - updateBakerRestakeEarnings :: (AccountVersionFor (MPV m) ~ 'AccountV0) => AccountIndex -> Bool -> m BakerRestakeEarningsUpdateResult - - -- * Operations on account keys +-- | Index that keeps track of modifications of smart contracts inside a single +-- transaction. This is used to cheaply detect whether a contract state has +-- changed or not when a contract calls another. +type ModificationIndex = Word - -- | Updates the credential verification keys - -- Preconditions: - -- * The account exists - -- * The account has keys defined at the specified indices - updateCredentialKeys :: AccountIndex -> ID.CredentialIndex -> ID.CredentialPublicKeys -> m () +-- | A modified state of a V1 instance. This is the state that is maintained +-- during the execution of a transaction. +-- +-- The type parameter `mr` is a technical necessity since we have to maintain a +-- new module interface. Since modules are parametrized by the monad (i.e., +-- either persistent or basic) we need to parametrize this state update as well, +-- seeing that the scheduler works with any state. On top of this, we often have +-- "newtype wrappers" @t m@ around a monad @m@ but with the property that +-- @InstrumentedModuleRef (t m) ~ InstrumentedModuleRef m@. In order for this to +-- work we actually need to parametrize the @InstanceV1Update'@ by a type +-- function @mr@ so that the typechecker can see the property that if +-- +-- @InstrumentedModuleRef (t m) ~ InstrumentedModuleRef m@ +-- +-- then also +-- +-- @InstanceV1Update (t m) ~ InstanceV1Update m@. +-- +-- That is why we have the auxiliary type definition @InstanceV1Update'@ +-- parametrized by the type function @mr@ and then a simplified type alias +-- @InstanceV1Update@ on top. +data InstanceV1Update' mr = InstanceV1Update + { -- | The modification index. + index :: !ModificationIndex, + -- | Amount changed + amountChange :: !AmountDelta, + -- | Present if a state change has ocurred. + newState :: !(Maybe (UpdatableContractState GSWasm.V1)), + -- | Present if the contract has been upgraded. + -- Contract upgrades are only supported from PV 5 and onwards. + newInterface :: !(Maybe (GSWasm.ModuleInterfaceA (mr GSWasm.V1), Set.Set GSWasm.ReceiveName)) + } - -- * Chain updates +type InstanceV1Update m = InstanceV1Update' (InstrumentedModuleRef m) - -- | Get the current authorized keys for updates. - getUpdateKeyCollection :: m (UpdateKeysCollection (AuthorizationsVersionFor (MPV m))) +type ChangeSet m = ChangeSet' (InstrumentedModuleRef m) - -- | Get the next sequence number of updates of a given type. - getNextUpdateSequenceNumber :: UpdateType -> m UpdateSequenceNumber +-- | The set of changes to be committed on a successful transaction. +-- +-- The reason for parametrizing by a type function @mr@ is the same as for +-- @InstanceV1Update@. +data ChangeSet' mr = ChangeSet + { -- | Accounts whose states changed. + -- |V0 contracts whose states changed. Any time we are updating a contract we know which version it is. + -- We thus know where to look. + _accountUpdates :: !(HMap.HashMap AccountIndex AccountUpdate), + _instanceV0Updates :: !(HMap.HashMap ContractAddress (ModificationIndex, AmountDelta, Maybe (UpdatableContractState GSWasm.V0))), + -- | V1 contracts whose state changed (and/or) has been upgraded. Any time we are updating a contract we know which version it is. + -- We thus know where to look. + _instanceV1Updates :: !(HMap.HashMap ContractAddress (InstanceV1Update' mr)), + -- | Contracts that were initialized. + _instanceInits :: !(HSet.HashSet ContractAddress), + -- | Change in the encrypted balance of the system as a result of this contract's execution. + _encryptedChange :: !AmountDelta, + -- | The release schedules added to accounts on this block, to be added on the per block map. + _addedReleaseSchedules :: !(Map.Map AccountAddress Timestamp) + } - -- | Add an update to the relevant update queue. The update is - -- assumed to have the next sequence number for its update type. - -- The next sequence number will be correspondingly incremented, - -- and any queued updates of the given type with a later effective - -- time are cancelled. - enqueueUpdate :: TransactionTime -> UpdateValue (ChainParametersVersionFor (MPV m)) (AuthorizationsVersionFor (MPV m)) -> m () +makeLenses ''ChangeSet' - -- | 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. - incrementPLTUpdateSequenceNumber :: (PVSupportsPLT (MPV m)) => m () +-- * Scheduler operations + +-- | Get the 'AccountIndex' for an account, if it exists. +getAccountIndex :: + (BS.BlockStateOperations m) => + AccountAddress -> SchedulerT m (Maybe AccountIndex) +{-# INLINE getAccountIndex #-} +getAccountIndex addr = lift . flip BS.bsoGetAccountIndex addr =<< use ssBlockState + +-- | Check whether the given account address would clash with any existing +-- account's address. The behaviour of this will generally depend on the +-- protocol version. +addressWouldClash :: + (BS.BlockStateOperations m) => + AccountAddress -> SchedulerT m Bool +{-# INLINE addressWouldClash #-} +addressWouldClash !addr = + lift . flip BS.bsoAddressWouldClash addr =<< use ssBlockState + +-- | Commit to global state all the updates to local state that have +-- accumulated through the execution. This method is also in charge of +-- recording which accounts were affected by the transaction for reward and +-- other purposes. +-- Precondition: Each account affected in the change set must exist in the +-- block state. +commitChanges :: (BS.BlockStateOperations m) => ChangeSet m -> SchedulerT m () +{-# INLINE commitChanges #-} +commitChanges !cs = do + s <- use ssBlockState + -- ASSUMPTION: the property which should hold at this point is that any + -- changed instance must exist in the global state and moreover all instances + -- are distinct by the virtue of a HashMap being a function + s1 <- + lift + ( foldM + ( \s' (addr, (modIdx, amnt, val)) -> + -- If the modification index is 0, this means that we have only recorded the + -- state in the changeset because we needed to due to calls to other contracts, + -- but the state of the instance did not change. So we don't have to modify the + -- instance. + if modIdx /= 0 then BS.bsoModifyInstance s' addr amnt val Nothing else return s' + ) + s + (HMap.toList (cs ^. instanceV0Updates)) + ) + -- since V0 and V1 instances are disjoint, the order in which we do updates does not matter. + s2 <- + lift + ( foldM + ( \s' (addr, InstanceV1Update{..}) -> + BS.bsoModifyInstance s' addr amountChange newState newInterface + ) + s1 + (HMap.toList (cs ^. instanceV1Updates)) + ) + -- Notify account transfers. + -- This also updates the release schedule. + s3 <- + lift + ( foldM + BS.bsoModifyAccount + s2 + (cs ^. accountUpdates) + ) + ssBlockState .= s3 + +-- | Commit a module interface and module value to global state. Returns @True@ +-- if this was successful, and @False@ if a module with the given Hash already +-- existed. Also store the code of the module for archival purposes. +commitModule :: (IsWasmVersion v, BS.BlockStateOperations m) => (GSWasm.ModuleInterfaceV v, Wasm.WasmModuleV v) -> SchedulerT m Bool +{-# INLINE commitModule #-} +commitModule !iface = do + (res, s') <- lift . (\s -> BS.bsoPutNewModule s iface) =<< use ssBlockState + ssBlockState .= s' + return res + +-- | Create new instance in the global state. +-- The instance is parametrised by the address, and the return value is the +-- address assigned to the new instance. +putNewInstance :: + (IsWasmVersion v, BS.BlockStateOperations m) => + NewInstanceData (InstrumentedModuleRef m v) v -> SchedulerT m ContractAddress +{-# INLINE putNewInstance #-} +putNewInstance !mkInstance = do + (caddr, s') <- lift . flip BS.bsoPutNewInstance mkInstance =<< use ssBlockState + ssBlockState .= s' + return caddr + +-- | Bump the next available transaction nonce of the account. +-- Precondition: the account exists in the block state. +increaseAccountNonce :: (BS.BlockStateOperations m) => IndexedAccount m -> SchedulerT m () +{-# INLINE increaseAccountNonce #-} +increaseAccountNonce (ai, acc) = do + s <- use ssBlockState + nonce <- BS.getAccountNonce acc + s' <- lift (BS.bsoModifyAccount s (emptyAccountUpdate ai & auNonce ?~ (nonce + 1))) + ssBlockState .= s' + +-- | Update account credentials. +-- Preconditions: +-- - The account exists in the block state. +-- - The account threshold is reasonable. +updateAccountCredentials :: + (BS.BlockStateOperations m) => + AccountIndex -> + -- | The indices of credentials to remove from the account. + [ID.CredentialIndex] -> + -- | The new credentials to add. + Map.Map ID.CredentialIndex ID.AccountCredential -> + -- | The new account threshold + ID.AccountThreshold -> + SchedulerT m () +{-# INLINE updateAccountCredentials #-} +updateAccountCredentials !ai !idcs !creds !threshold = do + s <- use ssBlockState + s' <- lift (BS.bsoUpdateAccountCredentials s ai idcs creds threshold) + ssBlockState .= s' + +-- | Create and add an empty account with the given public key, address and credential. +-- If an account with the given address already exists, @Nothing@ is returned. +-- Otherwise, the new account is returned, and the credential is added to the known credentials. +-- +-- It is not checked if the account's credential is a duplicate. +createAccount :: + (BS.BlockStateOperations m) => + CryptographicParameters -> AccountAddress -> ID.AccountCredential -> SchedulerT m (Maybe (Account m)) +{-# INLINE createAccount #-} +createAccount cparams addr credential = do + s <- use ssBlockState + (res, s') <- lift (BS.bsoCreateAccount s cparams addr credential) + ssBlockState .= s' + return res + +-- | Notify energy used by the current execution. +-- Add to the current running total of energy used. +markEnergyUsed :: (Monad m) => Energy -> SchedulerT m () +{-# INLINE markEnergyUsed #-} +markEnergyUsed energy = ssEnergyUsed += energy + +-- | Get the currently used amount of block energy. +getUsedEnergy :: (Monad m) => SchedulerT m Energy +{-# INLINE getUsedEnergy #-} +getUsedEnergy = use ssEnergyUsed + +getRemainingEnergy :: (BS.BlockStateOperations m) => SchedulerT m Energy +getRemainingEnergy = do + maxEnergy <- getMaxBlockEnergy + usedEnergy <- getUsedEnergy + return $! if usedEnergy <= maxEnergy then maxEnergy - usedEnergy else 0 + +-- | Get the next transaction index in the block, and increase the internal counter +bumpTransactionIndex :: (Monad m) => SchedulerT m TransactionIndex +{-# INLINE bumpTransactionIndex #-} +bumpTransactionIndex = ssNextIndex <<%= (+ 1) + +-- | Record that the amount was charged for execution. Amount is distributed +-- at the end of block execution in accordance with the tokenomics principles. +notifyExecutionCost :: (Monad m) => Amount -> SchedulerT m () +{-# INLINE notifyExecutionCost #-} +notifyExecutionCost !amnt = ssExecutionCosts += amnt + +-- | Notify the state that an amount has been transferred from public to +-- encrypted or vice-versa. +notifyEncryptedBalanceChange :: (BS.BlockStateOperations m) => AmountDelta -> SchedulerT m () +{-# INLINE notifyEncryptedBalanceChange #-} +notifyEncryptedBalanceChange !amntDiff = do + s <- use ssBlockState + s' <- lift (BS.bsoNotifyEncryptedBalanceChange s amntDiff) + ssBlockState .= s' + +-- | Convert the given energy amount into an amount of GTU. The exchange +-- rate can vary depending on the current state of the blockchain. +energyToGtu :: (BS.BlockStateOperations m) => Energy -> SchedulerT m Amount +{-# INLINE energyToGtu #-} +energyToGtu v = do + s <- use ssBlockState + rate <- lift $ _erEnergyRate <$> BS.bsoGetExchangeRates s + return $! computeCost rate v + +-- * Operations related to bakers. + +-- | Register this account as a baker. +-- The following results are possible: +-- +-- * @BASuccess id@: the baker was created with the specified 'BakerId'. +-- @id@ is always chosen to be the account index. +-- +-- * @BAInvalidAccount@: the address does not resolve to a valid account. +-- +-- * @BAAlreadyBaker@: the account is already registered as a baker. +-- +-- * @BAInsufficientBalance@: the balance on the account is insufficient to +-- stake the specified amount. +-- +-- * @BADuplicateAggregationKey@: the aggregation key is already in use. +-- +-- Note that if two results could apply, the first in this list takes precedence. +addBaker :: + ( AccountVersionFor (MPV m) ~ 'AccountV0, + ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, + BS.BlockStateOperations m + ) => + AccountIndex -> + BakerAdd -> + SchedulerT m BakerAddResult +{-# INLINE addBaker #-} +addBaker ai badd = do + s <- use ssBlockState + (ret, s') <- lift (BS.bsoAddBaker s ai badd) + ssBlockState .= s' + return ret + +-- | From chain parameters version 1, this operation adds a validator on an account. +-- For details of the behaviour and return values, see +-- 'Concordium.GlobalState.BlockState.bsoAddValidator'. +-- +-- PRECONDITION: +-- * The account must exist; +-- * The account must not already be a validator; +-- * The flag must indicate if the account is currently a delegator, which will be removed; +-- * The account must have sufficient balance to cover the stake. +addValidator :: + (PVSupportsDelegation (MPV m), BS.BlockStateOperations m) => + AccountIndex -> + -- | Whether the account already has a delegator, which will be removed in the process. + RemoveExistingStake -> + ValidatorAdd -> + SchedulerT m (Either ValidatorConfigureFailure ()) +{-# INLINE addValidator #-} +addValidator ai removeDelegator vadd = do + s <- use ssBlockState + (s', res) <- lift (doAdd s) + ssBlockState .= s' + return res + where + doAdd s0 | RemoveExistingStake ts <- removeDelegator = do + -- We need to remove the delegator first. + -- We take a snapshot of the state so we can rollback if the add fails. + snapshot <- BS.bsoSnapshotState s0 + rdRes <- BS.bsoUpdateDelegator s0 ts ai delegatorRemove + case rdRes of + Left e -> + -- Removing the delegator cannot fail, since the account must have a delegator. + error $ "addValidator: Failed to remove delegator: " ++ show e + Right (_, s1) -> do + res <- BS.bsoAddValidator s1 ai vadd + case res of + Left e -> do + -- Rollback the state to the snapshot. + s' <- BS.bsoRollback s1 snapshot + return (s', Left e) + Right s' -> return (s', Right ()) + doAdd s = do + res <- BS.bsoAddValidator s ai vadd + return $! case res of + Left e -> (s, Left e) + Right s' -> (s', Right ()) + +-- | From chain parameters version 1, this operation updates or removes a validator on an +-- account. For details of the behaviour and return values, see +-- 'Concordium.GlobalState.BlockState.bsoUpdateValidator'. +-- +-- PRECONDITION: +-- * The account must exist; +-- * The account must be a validator; +-- * The account must have sufficient balance to cover the new stake. +updateValidator :: + (PVSupportsDelegation (MPV m), BS.BlockStateOperations m) => + Timestamp -> + AccountIndex -> + ValidatorUpdate -> + SchedulerT m (Either ValidatorConfigureFailure [BakerConfigureUpdateChange]) +{-# INLINE updateValidator #-} +updateValidator ts ai vadd = do + s <- use ssBlockState + lift (BS.bsoUpdateValidator s ts ai vadd) >>= \case + Left e -> return (Left e) + Right (events, s') -> do + ssBlockState .= s' + return (Right events) + +-- | From chain parameters version 1, this operation adds a delegator on an account. +-- For details of the behaviour and return values, see +-- 'Concordium.GlobalState.BlockState.bsoAddDelegator'. +-- +-- PRECONDITION: +-- * The account must exist; +-- * The account must not already be a delegator; +-- * The flag must indicate if the account is currently a validator, which will be removed; +-- * The account must have sufficient balance to cover the stake. +addDelegator :: + (PVSupportsDelegation (MPV m), BS.BlockStateOperations m) => + AccountIndex -> + -- | Whether the account already has a validator, which will be removed in the process. + RemoveExistingStake -> + DelegatorAdd -> + SchedulerT m (Either DelegatorConfigureFailure ()) +{-# INLINE addDelegator #-} +addDelegator ai removeValidator dadd = do + s <- use ssBlockState + (s', res) <- lift (doAdd s) + ssBlockState .= s' + return res + where + doAdd s0 | RemoveExistingStake ts <- removeValidator = do + -- We need to remove the validator first. + -- We take a snapshot of the state so we can rollback if the add fails. + snapshot <- BS.bsoSnapshotState s0 + rvRes <- BS.bsoUpdateValidator s0 ts ai validatorRemove + case rvRes of + Left e -> + -- Removing the validator cannot fail, since the account must have a validator. + error $ "addDelegator: Failed to remove validator: " ++ show e + Right (_, s1) -> do + res <- BS.bsoAddDelegator s1 ai dadd + case res of + Left e -> do + -- Rollback the state to the snapshot. + s' <- BS.bsoRollback s1 snapshot + return (s', Left e) + Right s' -> return (s', Right ()) + doAdd s = do + res <- BS.bsoAddDelegator s ai dadd + return $! case res of + Left e -> (s, Left e) + Right s' -> (s', Right ()) + +-- | From chain parameters version 1, this operation updates or removes a delegator on an +-- account. For details of the behaviour and return values, see +-- 'Concordium.GlobalState.BlockState.bsoUpdateDelegator'. +-- +-- PRECONDITION: +-- * The account must exist; +-- * The account must be a delegator; +-- * The account must have sufficient balance to cover the new stake. +updateDelegator :: + (PVSupportsDelegation (MPV m), BS.BlockStateOperations m) => + Timestamp -> + AccountIndex -> + DelegatorUpdate -> + SchedulerT m (Either DelegatorConfigureFailure [DelegationConfigureUpdateChange]) +{-# INLINE updateDelegator #-} +updateDelegator ts ai dadd = do + s <- use ssBlockState + lift (BS.bsoUpdateDelegator s ts ai dadd) >>= \case + Left e -> return (Left e) + Right (events, s') -> do + ssBlockState .= s' + return (Right events) + +-- | Remove the baker associated with an account. +-- The removal takes effect after a cooling-off period. +-- Removal may fail if the baker is already cooling-off from another change (e.g. stake reduction). +-- +-- The following results are possible: +-- +-- * @BRRemoved e@: the baker was removed, and will be in cooling-off until epoch @e@. +-- The change will take effect in epoch @e+1@. +-- +-- * @BRInvalidBaker@: the account address is not valid, or the account is not a baker. +-- +-- * @BRChangePending@: the baker is currently in a cooling-off period and so cannot be removed. +removeBaker :: + ( AccountVersionFor (MPV m) ~ 'AccountV0, + ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, + BS.BlockStateOperations m + ) => + AccountIndex -> + SchedulerT m BakerRemoveResult +{-# INLINE removeBaker #-} +removeBaker ai = do + s <- use ssBlockState + (ret, s') <- lift (BS.bsoRemoveBaker s ai) + ssBlockState .= s' + return ret + +-- | Update the keys associated with an account. +-- It is assumed that the keys have already been checked for validity/ownership as +-- far as is necessary. +-- The only check on the keys is that the aggregation key is not a duplicate. +-- +-- The following results are possible: +-- +-- * @BKUSuccess@: the keys were updated +-- +-- * @BKUInvalidBaker@: the account does not exist or is not currently a baker. +-- +-- * @BKUDuplicateAggregationKey@: the aggregation key is a duplicate. +updateBakerKeys :: + ( AccountVersionFor (MPV m) ~ 'AccountV0, + ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, + BS.BlockStateOperations m + ) => + AccountIndex -> + BakerKeyUpdate -> + SchedulerT m BakerKeyUpdateResult +{-# INLINE updateBakerKeys #-} +updateBakerKeys ai keyUpd = do + s <- use ssBlockState + (r, s') <- lift (BS.bsoUpdateBakerKeys s ai keyUpd) + ssBlockState .= s' + return r + +-- | Update the stake associated with an account. +-- A reduction in stake will be delayed by the current cool-off period. +-- A change will not be made if there is already a cooling-off change +-- pending for the baker. +-- +-- The following results are possible: +-- +-- * @BSUStakeIncreased@: the baker's stake was increased. +-- This will take effect in the epoch after next. +-- +-- * @BSUStakeReduced e@: the baker's stake was reduced. +-- This will cool-off until epoch @e@ and take effect in epoch @e+1@. +-- +-- * @BSUStakeUnchanged@: there is no change to the baker's stake, but this update was successful. +-- +-- * @BSUInvalidBaker@: the account does not exist, or is not currently a baker. +-- +-- * @BSUChangePending@: the change could not be made since the account is already in a cooling-off period. +-- +-- * @BSUInsufficientBalance@: the account does not have sufficient balance to cover the staked amount. +updateBakerStake :: + ( AccountVersionFor (MPV m) ~ 'AccountV0, + ChainParametersVersionFor (MPV m) ~ 'ChainParametersV0, + BS.BlockStateOperations m + ) => + AccountIndex -> + Amount -> + SchedulerT m BakerStakeUpdateResult +{-# INLINE updateBakerStake #-} +updateBakerStake bi bsu = do + s <- use ssBlockState + (r, s') <- lift (BS.bsoUpdateBakerStake s bi bsu) + ssBlockState .= s' + return r + +-- | Update whether the baker automatically restakes the rewards it earns. +-- +-- The following results are possible: +-- +-- * @BREUUpdated id@: the flag was updated. +-- +-- * @BREUInvalidBaker@: the account does not exists, or is not currently a baker. +updateBakerRestakeEarnings :: + (AccountVersionFor (MPV m) ~ 'AccountV0, BS.BlockStateOperations m) => + AccountIndex -> Bool -> SchedulerT m BakerRestakeEarningsUpdateResult +{-# INLINE updateBakerRestakeEarnings #-} +updateBakerRestakeEarnings bi bre = do + s <- use ssBlockState + (r, s') <- lift (BS.bsoUpdateBakerRestakeEarnings s bi bre) + ssBlockState .= s' + return r + +-- * Operations on account keys + +-- | Updates the credential verification keys +-- Preconditions: +-- * The account exists +-- * The account has keys defined at the specified indices +updateCredentialKeys :: + (BS.BlockStateOperations m) => + AccountIndex -> ID.CredentialIndex -> ID.CredentialPublicKeys -> SchedulerT m () +{-# INLINE updateCredentialKeys #-} +updateCredentialKeys accIndex credIndex newKeys = do + s <- use ssBlockState + s' <- lift (BS.bsoSetAccountCredentialKeys s accIndex credIndex newKeys) + ssBlockState .= s' + +-- * Chain updates + +-- | Get the current authorized keys for updates. +getUpdateKeyCollection :: + (BS.BlockStateOperations m) => + SchedulerT m (UpdateKeysCollection (AuthorizationsVersionFor (MPV m))) +{-# INLINE getUpdateKeyCollection #-} +getUpdateKeyCollection = lift . BS.bsoGetUpdateKeyCollection =<< use ssBlockState + +-- | Get the next sequence number of updates of a given type. +getNextUpdateSequenceNumber :: + (BS.BlockStateOperations m) => + UpdateType -> SchedulerT m UpdateSequenceNumber +{-# INLINE getNextUpdateSequenceNumber #-} +getNextUpdateSequenceNumber uty = do + s <- use ssBlockState + lift (BS.bsoGetNextUpdateSequenceNumber s uty) + +-- | Add an update to the relevant update queue. The update is +-- assumed to have the next sequence number for its update type. +-- The next sequence number will be correspondingly incremented, +-- and any queued updates of the given type with a later effective +-- time are cancelled. +enqueueUpdate :: + (BS.BlockStateOperations m) => + TransactionTime -> + UpdateValue (ChainParametersVersionFor (MPV m)) (AuthorizationsVersionFor (MPV m)) -> + SchedulerT m () +{-# INLINE enqueueUpdate #-} +enqueueUpdate tt p = do + s <- use ssBlockState + s' <- lift (BS.bsoEnqueueUpdate s tt p) + ssBlockState .= s' + +-- | 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. +incrementPLTUpdateSequenceNumber :: + (PVSupportsPLT (MPV m), BS.BlockStateOperations m) => + SchedulerT m () +{-# INLINE incrementPLTUpdateSequenceNumber #-} +incrementPLTUpdateSequenceNumber = do + s <- use ssBlockState + s' <- lift (BS.bsoIncrementPLTUpdateSequenceNumber s) + ssBlockState .= s' + +-- | Get the 'TokenIndex' associated with a 'TokenId' (if it exists). +getTokenIndex :: (PVSupportsHaskellManagedPLT (MPV m), BS.BlockStateOperations m) => TokenId -> SchedulerT m (Maybe Token.TokenIndex) +{-# INLINE getTokenIndex #-} +getTokenIndex tokenId = do + blockState <- use ssBlockState + lift (BS.getTokenIndex blockState tokenId) + +-- | Get the configuration of a protocol-level token. +-- +-- PRECONDITION: The token identified by 'TokenIndex' MUST exist. +getTokenConfiguration :: + (PVSupportsHaskellManagedPLT (MPV m), BS.BlockStateOperations m) => + Token.TokenIndex -> SchedulerT m Token.PLTConfiguration +{-# INLINE getTokenConfiguration #-} +getTokenConfiguration tokenIndex = do + blockState <- use ssBlockState + lift (BS.getTokenConfiguration blockState tokenIndex) + +-- | Take a snapshot of the current block state, and run the given +-- computation. If the result is @(_, True)@, then the block state is +-- reverted to the snapshot. Otherwise, any changes to the block state are +-- retained. The return value is the result of the computation. +withBlockStateRollback :: (BS.BlockStateOperations m) => SchedulerT m (a, Bool) -> SchedulerT m a +withBlockStateRollback op = do + s0 <- use ssBlockState + snapshot <- lift $ BS.bsoSnapshotState s0 + (res, doRollback) <- op + when doRollback $ do + s1 <- use ssBlockState + s2 <- lift $ BS.bsoRollback s1 snapshot + ssBlockState .= s2 + return res + +-- | Run a protocol-layer token (PLT) operation that invokes the PLT kernel. +-- This call does not charge energy. +-- PRECONDITION: The 'TokenIndex' must be for a PLT that exists in the current state. +runPLT :: + forall m e a. + (PVSupportsHaskellManagedPLT (MPV m), BS.BlockStateOperations m) => + Token.TokenIndex -> + ( forall m1. + ( Monad m1, + PLTKernelPrivilegedUpdate m1, + PLTKernelFail e m1, + PLTAccount m1 ~ (AccountIndex, AccountAddress) + ) => + m1 a + ) -> + SchedulerT m (Either e a, [Event]) +runPLT tokenIx op = do + s <- use ssBlockState + let initialExecutionState = + PLTExecutionState + { _plteBlockState = s, + _plteEvents = [], + _plteEnergyUsed = 0, + _plteStateIsDirty = False + } + mutState <- lift $ BS.getMutableTokenState s tokenIx + (res, finalExecutionState) <- lift $ do + config <- BS.getTokenConfiguration s tokenIx + let context = + PLTExecutionContext + { _pltecTokenIndex = tokenIx, + _pltecConfiguration = config, + _pltecEnergy = Nothing, + _pltecMutableState = mutState + } + runKernelT op context initialExecutionState + when (isRight res && finalExecutionState ^. plteStateIsDirty) $ do + let outBlockState = finalExecutionState ^. plteBlockState + newBlockState <- lift $ BS.bsoSetTokenState outBlockState tokenIx mutState + ssBlockState .= newBlockState + return (res, reverse $ finalExecutionState ^. plteEvents) + +-- | Run a protocol-layer token (PLT) operation that invokes the PLT kernel +-- and uses at the maximum the specified amount of energy. Returns the result of +-- the computation together with the used energy. +-- +-- PRECONDITION: The 'TokenIndex' must be for a PLT that exists in the current state. +runPLTWithEnergy :: + forall m e a. + (PVSupportsHaskellManagedPLT (MPV m), BS.BlockStateOperations m) => + Token.TokenIndex -> + Energy -> + ( forall m1. + ( Monad m1, + PLTKernelPrivilegedUpdate m1, + PLTKernelFail e m1, + PLTKernelChargeEnergy m1, + PLTAccount m1 ~ (AccountIndex, AccountAddress) + ) => + m1 a + ) -> + SchedulerT m (Either (PLTExecutionError e) a, [Event], Energy) +runPLTWithEnergy tokenIx energy op = do + s <- use ssBlockState + let initialExecutionState = + PLTExecutionState + { _plteBlockState = s, + _plteEvents = [], + _plteEnergyUsed = 0, + _plteStateIsDirty = False + } + mutState <- lift $ BS.getMutableTokenState s tokenIx + (res, finalExecutionState) <- lift $ do + config <- BS.getTokenConfiguration s tokenIx + let context = + PLTExecutionContext + { _pltecTokenIndex = tokenIx, + _pltecConfiguration = config, + _pltecEnergy = Just energy, + _pltecMutableState = mutState + } + runKernelT op context initialExecutionState + when (isRight res && finalExecutionState ^. plteStateIsDirty) $ do + let outBlockState = finalExecutionState ^. plteBlockState + newBlockState <- lift $ BS.bsoSetTokenState outBlockState tokenIx mutState + ssBlockState .= newBlockState + return (res, reverse $ finalExecutionState ^. plteEvents, finalExecutionState ^. plteEnergyUsed) + +-- | Get the block state in the scheduler monad. +-- +-- This is a Low-level interface needed for foreign function interface access. +getBlockState :: (Monad m) => SchedulerT m (UpdatableBlockState m) +{-# INLINE getBlockState #-} +getBlockState = use ssBlockState - -- | Get the 'TokenIndex' associated with a 'TokenId' (if it exists). - getTokenIndex :: (PVSupportsPLT (MPV m)) => TokenId -> m (Maybe Token.TokenIndex) +-- | Set the block state in the scheduler monad. +-- +-- This is a Low-level interface needed for foreign function interface access. +setBlockState :: (Monad m) => UpdatableBlockState m -> SchedulerT m () +{-# INLINE setBlockState #-} +setBlockState = (ssBlockState .=) - -- | Get the configuration of a protocol-level token. - -- - -- PRECONDITION: The token identified by 'TokenIndex' MUST exist. - getTokenConfiguration :: (PVSupportsPLT (MPV m)) => Token.TokenIndex -> m Token.PLTConfiguration - - -- | Take a snapshot of the current block state, and run the given - -- computation. If the result is @(_, True)@, then the block state is - -- reverted to the snapshot. Otherwise, any changes to the block state are - -- retained. The return value is the result of the computation. - withBlockStateRollback :: m (a, Bool) -> m a - - -- | Run a protocol-layer token (PLT) operation that invokes the PLT kernel. - -- This call does not charge energy. - -- PRECONDITION: The 'TokenIndex' must be for a PLT that exists in the current state. - runPLT :: - (PVSupportsPLT (MPV m)) => - Token.TokenIndex -> - ( forall m1. - ( Monad m1, - PLTKernelPrivilegedUpdate m1, - PLTKernelFail e m1, - PLTAccount m1 ~ (AccountIndex, AccountAddress) - ) => - m1 a - ) -> - m (Either e a, [Event]) - - -- | Run a protocol-layer token (PLT) operation that invokes the PLT kernel - -- and uses at the maximum the specified amount of energy. Returns the result of - -- the computation together with the used energy. - -- - -- PRECONDITION: The 'TokenIndex' must be for a PLT that exists in the current state. - runPLTWithEnergy :: - (PVSupportsPLT (MPV m)) => - Token.TokenIndex -> - Energy -> - ( forall m1. - ( Monad m1, - PLTKernelPrivilegedUpdate m1, - PLTKernelFail e m1, - PLTKernelChargeEnergy m1, - PLTAccount m1 ~ (AccountIndex, AccountAddress) - ) => - m1 a - ) -> - m (Either (PLTExecutionError e) a, [Event], Energy) - - -- | Create a new protocol-layer token with the given 'PLTConfiguration'. - -- - -- PRECONDITION: There MUST NOT already be a token with the specified token ID. - -- The governance account index MUST reference a valid account. - createToken :: - (PVSupportsPLT (MPV m)) => - Token.PLTConfiguration -> - m Token.TokenIndex +-- | Create a new protocol-layer token with the given 'PLTConfiguration'. +-- +-- PRECONDITION: There MUST NOT already be a token with the specified token ID. +-- The governance account index MUST reference a valid account. +createToken :: + (PVSupportsHaskellManagedPLT (MPV m), BS.BlockStateOperations m) => + Token.PLTConfiguration -> + SchedulerT m Token.TokenIndex +createToken pltConfig = do + s <- use ssBlockState + (tokenIx, s') <- lift $ BS.bsoCreateToken s pltConfig + ssBlockState .= s' + return tokenIx -- | Contract state that is lazily thawed. This is used in the scheduler when -- looking up contracts. When looking them up first time we don't convert the @@ -661,71 +1225,6 @@ class (StaticInformation m, ContractStateOperations m, MonadProtocolVersion m) = Set.Set GSWasm.ReceiveName -> m () --- | Index that keeps track of modifications of smart contracts inside a single --- transaction. This is used to cheaply detect whether a contract state has --- changed or not when a contract calls another. -type ModificationIndex = Word - --- | A modified state of a V1 instance. This is the state that is maintained --- during the execution of a transaction. --- --- The type parameter `mr` is a technical necessity since we have to maintain a --- new module interface. Since modules are parametrized by the monad (i.e., --- either persistent or basic) we need to parametrize this state update as well, --- seeing that the scheduler works with any state. On top of this, we often have --- "newtype wrappers" @t m@ around a monad @m@ but with the property that --- @InstrumentedModuleRef (t m) ~ InstrumentedModuleRef m@. In order for this to --- work we actually need to parametrize the @InstanceV1Update'@ by a type --- function @mr@ so that the typechecker can see the property that if --- --- @InstrumentedModuleRef (t m) ~ InstrumentedModuleRef m@ --- --- then also --- --- @InstanceV1Update (t m) ~ InstanceV1Update m@. --- --- That is why we have the auxiliary type definition @InstanceV1Update'@ --- parametrized by the type function @mr@ and then a simplified type alias --- @InstanceV1Update@ on top. -data InstanceV1Update' mr = InstanceV1Update - { -- | The modification index. - index :: !ModificationIndex, - -- | Amount changed - amountChange :: !AmountDelta, - -- | Present if a state change has ocurred. - newState :: !(Maybe (UpdatableContractState GSWasm.V1)), - -- | Present if the contract has been upgraded. - -- Contract upgrades are only supported from PV 5 and onwards. - newInterface :: !(Maybe (GSWasm.ModuleInterfaceA (mr GSWasm.V1), Set.Set GSWasm.ReceiveName)) - } - -type InstanceV1Update m = InstanceV1Update' (InstrumentedModuleRef m) - -type ChangeSet m = ChangeSet' (InstrumentedModuleRef m) - --- | The set of changes to be committed on a successful transaction. --- --- The reason for parametrizing by a type function @mr@ is the same as for --- @InstanceV1Update@. -data ChangeSet' mr = ChangeSet - { -- | Accounts whose states changed. - -- |V0 contracts whose states changed. Any time we are updating a contract we know which version it is. - -- We thus know where to look. - _accountUpdates :: !(HMap.HashMap AccountIndex AccountUpdate), - _instanceV0Updates :: !(HMap.HashMap ContractAddress (ModificationIndex, AmountDelta, Maybe (UpdatableContractState GSWasm.V0))), - -- | V1 contracts whose state changed (and/or) has been upgraded. Any time we are updating a contract we know which version it is. - -- We thus know where to look. - _instanceV1Updates :: !(HMap.HashMap ContractAddress (InstanceV1Update' mr)), - -- | Contracts that were initialized. - _instanceInits :: !(HSet.HashSet ContractAddress), - -- | Change in the encrypted balance of the system as a result of this contract's execution. - _encryptedChange :: !AmountDelta, - -- | The release schedules added to accounts on this block, to be added on the per block map. - _addedReleaseSchedules :: !(Map.Map AccountAddress Timestamp) - } - -makeLenses ''ChangeSet' - emptyCS :: Proxy m -> ChangeSet m emptyCS Proxy = ChangeSet HMap.empty HMap.empty HMap.empty HSet.empty 0 Map.empty @@ -965,12 +1464,12 @@ data ExecutionCharge = ExecutionCharge -- for execution. -- This function assumes that the deposited energy is not less than the used energy. computeExecutionCharge :: - (SchedulerMonad m) => + (BS.BlockStateOperations m) => -- | Energy allocated. Energy -> -- | Energy remaining unused. Energy -> - m ExecutionCharge + SchedulerT m ExecutionCharge computeExecutionCharge allocated unused = do let ecUsedEnergy = allocated - unused ecEnergyCost <- energyToGtu ecUsedEnergy @@ -987,12 +1486,12 @@ computeExecutionCharge allocated unused = do -- - by 'dispatchTransactionBody' to charge for a payload that could not be deserialized. chargeExecutionCost :: forall m. - (SchedulerMonad m) => + (BS.BlockStateOperations m) => -- | Payer account.` IndexedAccount m -> -- | Execution cost. Amount -> - m () + SchedulerT m () chargeExecutionCost (ai, acc) amnt = do balance <- getAccountAmount acc let csWithAccountDelta = emptyCS (Proxy @m) & accountUpdates . at ai ?~ (emptyAccountUpdate ai & auAmount ?~ amountDiff 0 amnt) @@ -1003,12 +1502,12 @@ chargeExecutionCost (ai, acc) amnt = do -- | Compute the amount to charge the transaction payer for executing the transaction, -- and charge the account. Returns the energy and cost charged. computeChargeExecution :: - (SchedulerMonad m) => + (BS.BlockStateOperations m) => -- | The context for the transaction execution. WithDepositContext m -> -- | The remaining unused energy. Energy -> - m ExecutionCharge + SchedulerT m ExecutionCharge computeChargeExecution wtc unused = do executionCharge <- computeExecutionCharge (_wtcEnergyAmount wtc) unused chargeExecutionCost (_wtcPayerAccount wtc) (ecEnergyCost executionCharge) @@ -1035,7 +1534,9 @@ data WithDepositContext m = WithDepositContext -- | Energy currently used by the block. _wtcCurrentlyUsedBlockEnergy :: !Energy, -- | Index of the transaction in a block. - _wtcTransactionIndex :: !TransactionIndex + _wtcTransactionIndex :: !TransactionIndex, + -- | Sequence number (nonce) of the transaction, as specified in the transaction header. + _wtcTransactionSequenceNumber :: !Nonce } makeLenses ''WithDepositContext @@ -1052,16 +1553,16 @@ makeLenses ''WithDepositContext -- * The deposited amount is __at least__ Cost.checkHeader applied to the respective parameters (i.e., minimum transaction cost). withDeposit :: forall tov m res a. - (SchedulerMonad m, TransactionResult res, tov ~ TransactionOutcomesVersionFor (MPV m)) => + (BS.BlockStateOperations m, TransactionResult res, tov ~ TransactionOutcomesVersionFor (MPV m)) => WithDepositContext m -> -- | The computation to run in the modified environment with reduced amount on the initial account. - LocalT a m a -> + LocalT a (SchedulerT m) a -> -- | Continuation for the successful branch of the computation. -- The execution cost is charged before this is called. -- It gets the result of the previous computation as input, in particular the -- remaining energy and the ChangeSet. It should return the result. - (LocalState m -> a -> m res) -> - m (Maybe (TransactionSummary' tov res)) + (LocalState (SchedulerT m) -> a -> SchedulerT m res) -> + SchedulerT m (Maybe (TransactionSummary' tov res)) withDeposit wtc comp k = do let tsHash = wtc ^. wtcTransactionHash let totalEnergyToUse = wtc ^. wtcEnergyAmount @@ -1134,10 +1635,10 @@ withDeposit wtc comp k = do -- used energy and the used energy. {-# INLINE defaultSuccess #-} defaultSuccess :: - (SchedulerMonad m, TransactionResult res) => - LocalState m -> + (BS.BlockStateOperations m, TransactionResult res) => + LocalState (SchedulerT m) -> [Event] -> - m res + SchedulerT m res defaultSuccess = \ls res -> do commitChanges (ls ^. changeSet) return (transactionSuccess res) @@ -1513,7 +2014,7 @@ withExternalPure_ f = withExternal (return . fmap ((),) . f) -- | Helper function to log when a transaction was invalid. {-# INLINE logInvalidBlockItem #-} -logInvalidBlockItem :: (SchedulerMonad m) => BlockItem -> FailureKind -> m () +logInvalidBlockItem :: (MonadLogger m) => BlockItem -> FailureKind -> SchedulerT m () logInvalidBlockItem WithMetadata{wmdData = NormalTransaction{}, ..} fk = logEvent Scheduler LLWarning $ "Transaction with hash " ++ show wmdHash ++ " was invalid with reason: " ++ show fk logInvalidBlockItem WithMetadata{wmdData = CredentialDeployment cred} fk = @@ -1524,14 +2025,14 @@ logInvalidBlockItem WithMetadata{wmdData = ExtendedTransaction{}, ..} fk = logEvent Scheduler LLWarning $ "Transaction with hash " ++ show wmdHash ++ " was invalid with reason: " ++ show fk {-# INLINE logInvalidTransaction #-} -logInvalidTransaction :: (SchedulerMonad m) => TVer.TransactionWithStatus -> FailureKind -> m () +logInvalidTransaction :: (MonadLogger m) => TVer.TransactionWithStatus -> FailureKind -> SchedulerT m () logInvalidTransaction (WithMetadata{..}, _) fk = logEvent Scheduler LLWarning $ "Transaction with hash " ++ show wmdHash ++ " was invalid with reason: " ++ show fk -logInvalidCredential :: (SchedulerMonad m) => TVer.CredentialDeploymentWithStatus -> FailureKind -> m () +logInvalidCredential :: (MonadLogger m) => TVer.CredentialDeploymentWithStatus -> FailureKind -> SchedulerT m () logInvalidCredential (WithMetadata{..}, _) fk = logEvent Scheduler LLWarning $ "Credential with registration id " ++ (show . ID.credId . credential $ wmdData) ++ " was invalid with reason " ++ show fk -logInvalidChainUpdate :: (SchedulerMonad m) => TVer.ChainUpdateWithStatus -> FailureKind -> m () +logInvalidChainUpdate :: (MonadLogger m) => TVer.ChainUpdateWithStatus -> FailureKind -> SchedulerT m () logInvalidChainUpdate (WithMetadata{..}, _) fk = logEvent Scheduler LLWarning $ "Chain update with hash " ++ show wmdHash ++ " was invalid with reason: " ++ show fk diff --git a/concordium-consensus/src/Concordium/Scheduler/EnvironmentImplementation.hs b/concordium-consensus/src/Concordium/Scheduler/EnvironmentImplementation.hs deleted file mode 100644 index 5f21b790d6..0000000000 --- a/concordium-consensus/src/Concordium/Scheduler/EnvironmentImplementation.hs +++ /dev/null @@ -1,750 +0,0 @@ -{-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE UndecidableInstances #-} - --- | This module contains the implementation for running the scheduler computations. -module Concordium.Scheduler.EnvironmentImplementation where - -import Control.Monad -import Control.Monad.RWS.Strict -import Control.Monad.Trans.Cont -import Control.Monad.Trans.Reader (ReaderT (..)) -import Control.Monad.Trans.State.Strict (StateT (..)) -import Data.HashMap.Strict as Map -import qualified Data.Kind as DK -import Lens.Micro.Platform - -import Concordium.Types.Tokens - -import Concordium.GlobalState.Account -import qualified Concordium.GlobalState.BakerInfo as BI -import qualified Concordium.GlobalState.BlockState as BS -import Concordium.GlobalState.Persistent.Account.ProtocolLevelTokens -import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens (PLTConfiguration (..), TokenIndex) -import Concordium.GlobalState.TreeState -import Concordium.Logger -import Concordium.Scheduler.Environment -import Concordium.Scheduler.ProtocolLevelTokens.Kernel -import Concordium.Scheduler.Types -import Concordium.TimeMonad -import qualified Concordium.TransactionVerification as TVer -import Data.Either (isRight) - --- | Context for executing a scheduler computation. -data ContextState = ContextState - { -- | Chain metadata - _chainMetadata :: !ChainMetadata, - -- | Maximum allowed block energy. - _maxBlockEnergy :: !Energy, - -- | Maximum number of accounts to be created in the same block. - _accountCreationLimit :: !CredentialsPerBlockLimit - } - -makeLenses ''ContextState - --- | State accumulated during execution of a scheduler computation. -data SchedulerState (m :: DK.Type -> DK.Type) = SchedulerState - { -- | Current block state. - _ssBlockState :: !(UpdatableBlockState m), - -- | Energy used so far. - _ssEnergyUsed :: !Energy, - -- | The total execution costs so far. - _ssExecutionCosts :: !Amount, - -- | The next available transaction index. - _ssNextIndex :: !TransactionIndex - } - -makeLenses ''SchedulerState - --- | Create an initial state for running a scheduler computation. -makeInitialSchedulerState :: UpdatableBlockState m -> SchedulerState m -makeInitialSchedulerState _ssBlockState = - SchedulerState - { _ssEnergyUsed = 0, - _ssExecutionCosts = 0, - _ssNextIndex = 0, - .. - } - -data PLTExecutionState m = PLTExecutionState - { -- | The current block state. - _plteBlockState :: !(UpdatableBlockState m), - -- | The events that have been emitted during the execution in reverse order. - _plteEvents :: ![Event], - -- | The energy used for the execution of the PLT module. - _plteEnergyUsed :: !Energy, - -- | Tracking the token mutable state has been updated during the execution. - _plteStateIsDirty :: !Bool - } -makeLenses ''PLTExecutionState - --- | Execution context for PLT computations. -data PLTExecutionContext m = PLTExecutionContext - { -- | The token index. - _pltecTokenIndex :: !TokenIndex, - -- | The PLT configuration. - _pltecConfiguration :: !PLTConfiguration, - -- | The available energy. - _pltecEnergy :: !(Maybe Energy), - -- | The mutable token state. - _pltecMutableState :: !(MutableTokenState m) - } - --- | Alias for the internal type used in @SchedulerT@. -type InternalSchedulerT m = RWST ContextState () (SchedulerState m) - --- | Scheduler monad transformer. Extends a monad with the ability to execute scheduler computations. --- Use @runSchedulerT@ to run the computation. -newtype SchedulerT (m :: DK.Type -> DK.Type) (a :: DK.Type) = SchedulerT - { _runSchedulerT :: InternalSchedulerT m m a - } - deriving - ( Functor, - Applicative, - Monad, - MonadState (SchedulerState m), - MonadReader ContextState, - MonadLogger, - TimeMonad - ) - -instance MonadTrans SchedulerT where - {-# INLINE lift #-} - lift = SchedulerT . lift - -deriving via - (MGSTrans (InternalSchedulerT m) m) - instance - BlockStateTypes (SchedulerT m) - -instance (BS.BlockStateOperations m) => StaticInformation (SchedulerT m) where - {-# INLINE getMaxBlockEnergy #-} - getMaxBlockEnergy = view maxBlockEnergy - - {-# INLINE getChainMetadata #-} - getChainMetadata = view chainMetadata - - {-# INLINE getModuleInterfaces #-} - getModuleInterfaces mref = do - s <- use ssBlockState - lift (BS.bsoGetModule s mref) - - {-# INLINE getAccountCreationLimit #-} - getAccountCreationLimit = view accountCreationLimit - - {-# INLINE getContractInstance #-} - getContractInstance addr = lift . flip BS.bsoGetInstance addr =<< use ssBlockState - - {-# INLINE getStateAccount #-} - getStateAccount !addr = lift . flip BS.bsoGetAccount addr =<< use ssBlockState - - {-# INLINE getExchangeRates #-} - getExchangeRates = lift . BS.bsoGetExchangeRates =<< use ssBlockState - -deriving via - (MGSTrans (InternalSchedulerT m) m) - instance - (MonadProtocolVersion m) => MonadProtocolVersion (SchedulerT m) - -deriving via - (MGSTrans (InternalSchedulerT m) m) - instance - (BS.AccountOperations m) => BS.AccountOperations (SchedulerT m) - -deriving via - (MGSTrans (InternalSchedulerT m) m) - instance - (BS.ContractStateOperations m) => BS.ContractStateOperations (SchedulerT m) - -deriving via - (MGSTrans (InternalSchedulerT m) m) - instance - (BS.ModuleQuery m) => BS.ModuleQuery (SchedulerT m) - -instance - (BS.BlockStateOperations m, MonadProtocolVersion m) => - TVer.TransactionVerifier (SchedulerT m) - where - {-# INLINE registrationIdExists #-} - registrationIdExists !regid = - lift . flip BS.bsoRegIdExists regid =<< use ssBlockState - {-# INLINE getIdentityProvider #-} - getIdentityProvider !ipId = do - s <- use ssBlockState - lift (BS.bsoGetIdentityProvider s ipId) - {-# INLINE getAnonymityRevokers #-} - getAnonymityRevokers !arIds = do - s <- use ssBlockState - lift (BS.bsoGetAnonymityRevokers s arIds) - {-# INLINE getCryptographicParameters #-} - getCryptographicParameters = lift . BS.bsoGetCryptoParams =<< use ssBlockState - {-# INLINE getAccount #-} - getAccount !aaddr = do - s <- use ssBlockState - lift (fmap snd <$> BS.bsoGetAccount s aaddr) - {-# INLINE getNextUpdateSequenceNumber #-} - getNextUpdateSequenceNumber uType = lift . flip BS.bsoGetNextUpdateSequenceNumber uType =<< use ssBlockState - {-# INLINE getUpdateKeysCollection #-} - getUpdateKeysCollection = lift . BS.bsoGetUpdateKeyCollection =<< use ssBlockState - {-# INLINE getAccountAvailableAmount #-} - getAccountAvailableAmount = lift . BS.getAccountAvailableAmount - {-# INLINE getNextAccountNonce #-} - getNextAccountNonce = lift . BS.getAccountNonce - {-# INLINE getAccountVerificationKeys #-} - getAccountVerificationKeys = lift . BS.getAccountVerificationKeys - {-# INLINE energyToCcd #-} - energyToCcd v = do - s <- use ssBlockState - rate <- lift $ _erEnergyRate <$> BS.bsoGetExchangeRates s - return (computeCost rate v) - {-# INLINE getMaxBlockEnergy #-} - getMaxBlockEnergy = do - ctx <- ask - let maxEnergy = ctx ^. maxBlockEnergy - return maxEnergy - {-# INLINE checkExactNonce #-} - checkExactNonce = pure True - -instance - ( BS.BlockStateOperations m, - MonadLogger m, - MonadProtocolVersion m - ) => - SchedulerMonad (SchedulerT m) - where - {-# INLINE markEnergyUsed #-} - markEnergyUsed energy = ssEnergyUsed += energy - - {-# INLINE getUsedEnergy #-} - getUsedEnergy = use ssEnergyUsed - - {-# INLINE bumpTransactionIndex #-} - bumpTransactionIndex = ssNextIndex <<%= (+ 1) - - {-# INLINE getAccountIndex #-} - getAccountIndex addr = lift . flip BS.bsoGetAccountIndex addr =<< use ssBlockState - - {-# INLINE putNewInstance #-} - putNewInstance !mkInstance = do - (caddr, s') <- lift . flip BS.bsoPutNewInstance mkInstance =<< use ssBlockState - ssBlockState .= s' - return caddr - - {-# INLINE createAccount #-} - createAccount cparams addr credential = do - s <- use ssBlockState - (res, s') <- lift (BS.bsoCreateAccount s cparams addr credential) - ssBlockState .= s' - return res - - {-# INLINE addressWouldClash #-} - addressWouldClash !addr = - lift . flip BS.bsoAddressWouldClash addr =<< use ssBlockState - - {-# INLINE commitModule #-} - commitModule !iface = do - (res, s') <- lift . (\s -> BS.bsoPutNewModule s iface) =<< use ssBlockState - ssBlockState .= s' - return res - - {-# INLINE increaseAccountNonce #-} - increaseAccountNonce (ai, acc) = do - s <- use ssBlockState - nonce <- BS.getAccountNonce acc - s' <- lift (BS.bsoModifyAccount s (emptyAccountUpdate ai & auNonce ?~ (nonce + 1))) - ssBlockState .= s' - - {-# INLINE updateAccountCredentials #-} - updateAccountCredentials !ai !idcs !creds !threshold = do - s <- use ssBlockState - s' <- lift (BS.bsoUpdateAccountCredentials s ai idcs creds threshold) - ssBlockState .= s' - - {-# INLINE commitChanges #-} - commitChanges !cs = do - s <- use ssBlockState - -- ASSUMPTION: the property which should hold at this point is that any - -- changed instance must exist in the global state and moreover all instances - -- are distinct by the virtue of a HashMap being a function - s1 <- - lift - ( foldM - ( \s' (addr, (modIdx, amnt, val)) -> - -- If the modification index is 0, this means that we have only recorded the - -- state in the changeset because we needed to due to calls to other contracts, - -- but the state of the instance did not change. So we don't have to modify the - -- instance. - if modIdx /= 0 then BS.bsoModifyInstance s' addr amnt val Nothing else return s' - ) - s - (Map.toList (cs ^. instanceV0Updates)) - ) - -- since V0 and V1 instances are disjoint, the order in which we do updates does not matter. - s2 <- - lift - ( foldM - ( \s' (addr, InstanceV1Update{..}) -> - BS.bsoModifyInstance s' addr amountChange newState newInterface - ) - s1 - (Map.toList (cs ^. instanceV1Updates)) - ) - -- Notify account transfers. - -- This also updates the release schedule. - s3 <- - lift - ( foldM - BS.bsoModifyAccount - s2 - (cs ^. accountUpdates) - ) - ssBlockState .= s3 - - {-# INLINE energyToGtu #-} - energyToGtu v = do - s <- use ssBlockState - rate <- lift $ _erEnergyRate <$> BS.bsoGetExchangeRates s - return $! computeCost rate v - - {-# INLINE notifyExecutionCost #-} - notifyExecutionCost !amnt = ssExecutionCosts += amnt - - {-# INLINE notifyEncryptedBalanceChange #-} - notifyEncryptedBalanceChange !amntDiff = do - s <- use ssBlockState - s' <- lift (BS.bsoNotifyEncryptedBalanceChange s amntDiff) - ssBlockState .= s' - - {-# INLINE addBaker #-} - addBaker ai badd = do - s <- use ssBlockState - (ret, s') <- lift (BS.bsoAddBaker s ai badd) - ssBlockState .= s' - return ret - - {-# INLINE addValidator #-} - addValidator ai removeDelegator vadd = do - s <- use ssBlockState - (s', res) <- lift (doAdd s) - ssBlockState .= s' - return res - where - doAdd s0 | RemoveExistingStake ts <- removeDelegator = do - -- We need to remove the delegator first. - -- We take a snapshot of the state so we can rollback if the add fails. - snapshot <- BS.bsoSnapshotState s0 - rdRes <- BS.bsoUpdateDelegator s0 ts ai BI.delegatorRemove - case rdRes of - Left e -> - -- Removing the delegator cannot fail, since the account must have a delegator. - error $ "addValidator: Failed to remove delegator: " ++ show e - Right (_, s1) -> do - res <- BS.bsoAddValidator s1 ai vadd - case res of - Left e -> do - -- Rollback the state to the snapshot. - s' <- BS.bsoRollback s1 snapshot - return (s', Left e) - Right s' -> return (s', Right ()) - doAdd s = do - res <- BS.bsoAddValidator s ai vadd - return $! case res of - Left e -> (s, Left e) - Right s' -> (s', Right ()) - - {-# INLINE updateValidator #-} - updateValidator ts ai vadd = do - s <- use ssBlockState - lift (BS.bsoUpdateValidator s ts ai vadd) >>= \case - Left e -> return (Left e) - Right (events, s') -> do - ssBlockState .= s' - return (Right events) - - {-# INLINE addDelegator #-} - addDelegator ai removeValidator dadd = do - s <- use ssBlockState - (s', res) <- lift (doAdd s) - ssBlockState .= s' - return res - where - doAdd s0 | RemoveExistingStake ts <- removeValidator = do - -- We need to remove the validator first. - -- We take a snapshot of the state so we can rollback if the add fails. - snapshot <- BS.bsoSnapshotState s0 - rvRes <- BS.bsoUpdateValidator s0 ts ai BI.validatorRemove - case rvRes of - Left e -> - -- Removing the validator cannot fail, since the account must have a validator. - error $ "addDelegator: Failed to remove validator: " ++ show e - Right (_, s1) -> do - res <- BS.bsoAddDelegator s1 ai dadd - case res of - Left e -> do - -- Rollback the state to the snapshot. - s' <- BS.bsoRollback s1 snapshot - return (s', Left e) - Right s' -> return (s', Right ()) - doAdd s = do - res <- BS.bsoAddDelegator s ai dadd - return $! case res of - Left e -> (s, Left e) - Right s' -> (s', Right ()) - - {-# INLINE updateDelegator #-} - updateDelegator ts ai dadd = do - s <- use ssBlockState - lift (BS.bsoUpdateDelegator s ts ai dadd) >>= \case - Left e -> return (Left e) - Right (events, s') -> do - ssBlockState .= s' - return (Right events) - - {-# INLINE removeBaker #-} - removeBaker ai = do - s <- use ssBlockState - (ret, s') <- lift (BS.bsoRemoveBaker s ai) - ssBlockState .= s' - return ret - - {-# INLINE updateBakerKeys #-} - updateBakerKeys ai keyUpd = do - s <- use ssBlockState - (r, s') <- lift (BS.bsoUpdateBakerKeys s ai keyUpd) - ssBlockState .= s' - return r - - {-# INLINE updateBakerStake #-} - updateBakerStake bi bsu = do - s <- use ssBlockState - (r, s') <- lift (BS.bsoUpdateBakerStake s bi bsu) - ssBlockState .= s' - return r - - {-# INLINE updateBakerRestakeEarnings #-} - updateBakerRestakeEarnings bi bre = do - s <- use ssBlockState - (r, s') <- lift (BS.bsoUpdateBakerRestakeEarnings s bi bre) - ssBlockState .= s' - return r - - {-# INLINE updateCredentialKeys #-} - updateCredentialKeys accIndex credIndex newKeys = do - s <- use ssBlockState - s' <- lift (BS.bsoSetAccountCredentialKeys s accIndex credIndex newKeys) - ssBlockState .= s' - - {-# INLINE getUpdateKeyCollection #-} - getUpdateKeyCollection = lift . BS.bsoGetUpdateKeyCollection =<< use ssBlockState - - {-# INLINE getNextUpdateSequenceNumber #-} - getNextUpdateSequenceNumber uty = do - s <- use ssBlockState - lift (BS.bsoGetNextUpdateSequenceNumber s uty) - - {-# INLINE enqueueUpdate #-} - enqueueUpdate tt p = do - s <- use ssBlockState - s' <- lift (BS.bsoEnqueueUpdate s tt p) - ssBlockState .= s' - - {-# INLINE incrementPLTUpdateSequenceNumber #-} - incrementPLTUpdateSequenceNumber = do - s <- use ssBlockState - s' <- lift (BS.bsoIncrementPLTUpdateSequenceNumber s) - ssBlockState .= s' - - {-# INLINE getTokenIndex #-} - getTokenIndex tokenId = do - blockState <- use ssBlockState - lift (BS.getTokenIndex blockState tokenId) - - {-# INLINE getTokenConfiguration #-} - getTokenConfiguration tokenIndex = do - blockState <- use ssBlockState - lift (BS.getTokenConfiguration blockState tokenIndex) - - withBlockStateRollback op = do - s0 <- use ssBlockState - snapshot <- lift $ BS.bsoSnapshotState s0 - (res, doRollback) <- op - when doRollback $ do - s1 <- use ssBlockState - s2 <- lift $ BS.bsoRollback s1 snapshot - ssBlockState .= s2 - return res - - runPLT tokenIx op = do - s :: UpdatableBlockState m <- use ssBlockState - let initialExecutionState = - PLTExecutionState - { _plteBlockState = s, - _plteEvents = [], - _plteEnergyUsed = 0, - _plteStateIsDirty = False - } - mutState <- lift $ BS.getMutableTokenState s tokenIx - (res, finalExecutionState) <- lift $ do - config <- BS.getTokenConfiguration s tokenIx - let context = - PLTExecutionContext - { _pltecTokenIndex = tokenIx, - _pltecConfiguration = config, - _pltecEnergy = Nothing, - _pltecMutableState = mutState - } - runKernelT op context initialExecutionState - when (isRight res && finalExecutionState ^. plteStateIsDirty) $ do - let outBlockState = finalExecutionState ^. plteBlockState - newBlockState <- lift $ BS.bsoSetTokenState outBlockState tokenIx mutState - ssBlockState .= newBlockState - return (res, reverse $ finalExecutionState ^. plteEvents) - - runPLTWithEnergy tokenIx energy op = do - s :: UpdatableBlockState m <- use ssBlockState - let initialExecutionState = - PLTExecutionState - { _plteBlockState = s, - _plteEvents = [], - _plteEnergyUsed = 0, - _plteStateIsDirty = False - } - mutState <- lift $ BS.getMutableTokenState s tokenIx - (res, finalExecutionState) <- lift $ do - config <- BS.getTokenConfiguration s tokenIx - let context = - PLTExecutionContext - { _pltecTokenIndex = tokenIx, - _pltecConfiguration = config, - _pltecEnergy = Just energy, - _pltecMutableState = mutState - } - runKernelT op context initialExecutionState - when (isRight res && finalExecutionState ^. plteStateIsDirty) $ do - let outBlockState = finalExecutionState ^. plteBlockState - newBlockState <- lift $ BS.bsoSetTokenState outBlockState tokenIx mutState - ssBlockState .= newBlockState - return (res, reverse $ finalExecutionState ^. plteEvents, finalExecutionState ^. plteEnergyUsed) - - createToken pltConfig = do - s <- use ssBlockState - (tokenIx, s') <- lift $ BS.bsoCreateToken s pltConfig - ssBlockState .= s' - return tokenIx - --- | Execute the computation using the provided context and scheduler state. --- The return value is the value produced by the computation and the updated state of the scheduler. -runSchedulerT :: - (Monad m) => - SchedulerT m a -> - ContextState -> - SchedulerState m -> - m (a, SchedulerState m) -runSchedulerT computation contextState initialState = do - (value, resultingState, ()) <- runRWST (_runSchedulerT computation) contextState initialState - return (value, resultingState) - -newtype KernelT fail ret m a = KernelT {runKernelT' :: ReaderT (PLTExecutionContext m) (ContT (Either fail ret) (StateT (PLTExecutionState m) m)) a} - deriving - ( Functor, - Applicative, - Monad, - MonadState (PLTExecutionState m), - MonadReader (PLTExecutionContext m) - ) - -runKernelT :: (Monad m) => KernelT fail a m a -> PLTExecutionContext m -> PLTExecutionState m -> m (Either fail a, PLTExecutionState m) -runKernelT a tokenIx = runStateT (runContT (runReaderT (runKernelT' a) tokenIx) (return . Right)) - -instance MonadTrans (KernelT fail ret) where - lift = KernelT . lift . lift . lift - --- | The block state types for `KernelT fail ret m` are derived from the base monad `m`. -deriving via (MGSTrans (KernelT fail ret) m) instance BlockStateTypes (KernelT fail ret m) - -instance (BS.BlockStateOperations m, PVSupportsPLT (MPV m)) => PLTKernelQuery (KernelT fail ret m) where - type PLTAccount (KernelT fail ret m) = (AccountIndex, AccountAddress) - getTokenState key = do - mutableState <- asks _pltecMutableState - lift $ BS.lookupTokenState key mutableState - getAccount addr = do - bs <- use plteBlockState - lift $ fmap ((,addr) . fst) <$> BS.bsoGetAccount bs addr - getAccountIndex (acctIndex, _acct) = return acctIndex - getAccountByIndex accountIndex = do - bs <- use plteBlockState - lift $ - BS.bsoGetAccountByIndex bs accountIndex >>= \case - Nothing -> return Nothing - Just account -> do - address <- BS.getAccountCanonicalAddress account - return $ Just (accountIndex, address) - getAccountBalance (acctIndex, _) = do - tokenIx <- asks _pltecTokenIndex - bs <- use plteBlockState - lift $ - BS.bsoGetAccountByIndex bs acctIndex >>= \case - Nothing -> error "getAccountBalance: Account does not exist" - Just acct -> BS.getAccountTokenBalance acct tokenIx - getAccountCanonicalAddress (acctIndex, _) = do - bs <- use plteBlockState - lift $ - BS.bsoGetAccountByIndex bs acctIndex >>= \case - Nothing -> error "getAccountCanonicalAddress: Account does not exist" - Just acct -> BS.getAccountCanonicalAddress acct - getCirculatingSupply = do - tokenIx <- asks _pltecTokenIndex - bs <- use plteBlockState - lift $ BS.getTokenCirculatingSupply bs tokenIx - getDecimals = asks (_pltDecimals . _pltecConfiguration) - -instance (BS.BlockStateOperations m, PVSupportsPLT (MPV m)) => PLTKernelUpdate (KernelT fail ret m) where - setTokenState key mValue = do - plteStateIsDirty .= True - mutableState <- asks _pltecMutableState - lift $ BS.updateTokenState key mValue mutableState - - transfer (accIxFrom, accAddrFrom) (accIxTo, accAddrTo) amount mbMemo = do - context <- ask - let tokenIx = _pltecTokenIndex context - bs0 <- use plteBlockState - mbs2 <- lift $ do - mbs1 <- - BS.bsoUpdateTokenAccountBalance bs0 tokenIx accIxFrom $ - negativeTokenAmountDelta amount - case mbs1 of - Nothing -> return Nothing -- Sender has insufficient funds. - Just bs1 -> do - mbs2 <- - BS.bsoUpdateTokenAccountBalance bs1 tokenIx accIxTo $ - toTokenAmountDelta amount - return $ case mbs2 of - Nothing -> - -- This case cannot occur if the total supply is accurately - -- recorded, since it would imply that the total supply - -- exceeds the maximum representable amount. - error "Token kernel: transfer would overflow receiver balance" - Just bs2 -> - Just bs2 - case mbs2 of - Nothing -> return False - Just bs2 -> do - -- Log the transfer event. - plteEvents - %= ( TokenTransfer - { ettTokenId = _pltTokenId (_pltecConfiguration context), - ettFrom = HolderAccount accAddrFrom, - ettTo = HolderAccount accAddrTo, - ettAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)), - ettMemo = mbMemo - } - : - ) - plteBlockState .= bs2 - return True - - logTokenEvent eventType eventDetails = do - tokenId <- asks (_pltTokenId . _pltecConfiguration) - plteEvents %= (TokenModuleEvent tokenId eventType eventDetails :) - - touch (accIx, _) = do - context <- ask - let tokenIx = _pltecTokenIndex context - bs0 <- use plteBlockState - mbBs1 <- lift $ BS.bsoTouchTokenAccount bs0 tokenIx accIx - case mbBs1 of - Nothing -> return False - Just bs1 -> do - plteBlockState .= bs1 - return True - -instance (BS.BlockStateOperations m, PVSupportsPLT (MPV m)) => PLTKernelPrivilegedUpdate (KernelT fail ret m) where - mint (accIx, accAddr) amount = do - context <- ask - let tokenIx = _pltecTokenIndex context - bs <- use plteBlockState - currentSupply <- lift $ BS.getTokenCirculatingSupply bs tokenIx - if maxBound - amount < currentSupply - then return False -- Minting would overflow the circulating supply. - else do - bs' <- lift $ BS.bsoSetTokenCirculatingSupply bs tokenIx (currentSupply + amount) - mbNewBs <- - lift $ - BS.bsoUpdateTokenAccountBalance - bs' - tokenIx - accIx - (TokenAmountDelta (fromIntegral (theTokenRawAmount amount))) - case mbNewBs of - Nothing -> - -- This case cannot occur if the total supply is accurately - -- recorded, since it would imply that the total supply - -- exceeds the maximum representable amount. - error "Token kernel: mint would overflow receiver balance" - Just newBs -> do - -- Log the mint event. - plteEvents - %= ( TokenMint - { etmTokenId = _pltTokenId (_pltecConfiguration context), - etmTarget = HolderAccount accAddr, - etmAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)) - } - : - ) - plteBlockState .= newBs - return True - burn (accIx, accAddr) amount = do - context <- ask - let tokenIx = _pltecTokenIndex context - bs0 <- use plteBlockState - mbs1 <- lift $ BS.bsoUpdateTokenAccountBalance bs0 tokenIx accIx (negativeTokenAmountDelta amount) - case mbs1 of - Nothing -> return False - Just bs1 -> do - bs2 <- lift $ do - currentSupply <- BS.getTokenCirculatingSupply bs1 tokenIx - when (currentSupply < amount) $ - -- This case cannot occur if the total supply is accurately - -- recorded, since it would imply that the initial balance - -- on the target account exceeded to the total supply. - error "Token kernel: burn would underflow total supply" - BS.bsoSetTokenCirculatingSupply bs1 tokenIx (currentSupply - amount) - -- Log the burn event. - plteEvents - %= ( TokenBurn - { etbTokenId = _pltTokenId (_pltecConfiguration context), - etbTarget = HolderAccount accAddr, - etbAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)) - } - : - ) - plteBlockState .= bs2 - return True - -instance - ( BS.BlockStateOperations m, - PVSupportsPLT (MPV m) - ) => - PLTKernelChargeEnergy (KernelT (PLTExecutionError fail) ret m) - where - pltTickEnergy nrg = do - plteEnergyUsed += nrg - mbAvailableEnergy <- asks _pltecEnergy - case mbAvailableEnergy of - Nothing -> return () - Just availableEnergy -> do - energyUsed <- use plteEnergyUsed - unless (availableEnergy >= energyUsed) $ KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left PLTEOutOfEnergy) - -instance {-# OVERLAPPABLE #-} (Monad m, fail ~ fail') => (PLTKernelFail fail (KernelT fail' ret m)) where - -- To abort, we simply drop the continuation and return the error. - pltError err = KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left err) - -instance (Monad m) => (PLTKernelFail fail (KernelT (PLTExecutionError fail) ret m)) where - -- To abort, we simply drop the continuation and return the error. - pltError err = KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left $ PLTEFail err) diff --git a/concordium-consensus/src/Concordium/Scheduler/InvokeContract.hs b/concordium-consensus/src/Concordium/Scheduler/InvokeContract.hs index 59ef81d4b2..a523e717d0 100644 --- a/concordium-consensus/src/Concordium/Scheduler/InvokeContract.hs +++ b/concordium-consensus/src/Concordium/Scheduler/InvokeContract.hs @@ -39,7 +39,6 @@ import qualified Data.FixedByteString as FBS import Concordium.Scheduler import Concordium.Scheduler.Environment -import Concordium.Scheduler.EnvironmentImplementation (ContextState (..), accountCreationLimit, chainMetadata, maxBlockEnergy) import Concordium.Scheduler.Types import qualified Concordium.Scheduler.WasmIntegration.V1 as WasmV1 diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/KernelImplementation.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/KernelImplementation.hs new file mode 100644 index 0000000000..01bcbc7b90 --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/KernelImplementation.hs @@ -0,0 +1,256 @@ +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +module Concordium.Scheduler.ProtocolLevelTokens.KernelImplementation where + +import Control.Monad +import Control.Monad.RWS.Strict +import Control.Monad.Trans.Cont +import Control.Monad.Trans.Reader (ReaderT (..)) +import Control.Monad.Trans.State.Strict (StateT (..)) +import Lens.Micro.Platform + +import Concordium.Types.Tokens + +import qualified Concordium.GlobalState.BlockState as BS +import Concordium.GlobalState.Persistent.Account.ProtocolLevelTokens +import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens (PLTConfiguration (..), TokenIndex) +import Concordium.GlobalState.TreeState +import Concordium.Scheduler.ProtocolLevelTokens.Kernel +import Concordium.Scheduler.Types + +data PLTExecutionState m = PLTExecutionState + { -- | The current block state. + _plteBlockState :: !(UpdatableBlockState m), + -- | The events that have been emitted during the execution in reverse order. + _plteEvents :: ![Event], + -- | The energy used for the execution of the PLT module. + _plteEnergyUsed :: !Energy, + -- | Tracking the token mutable state has been updated during the execution. + _plteStateIsDirty :: !Bool + } +makeLenses ''PLTExecutionState + +-- | Execution context for PLT computations. +data PLTExecutionContext m = PLTExecutionContext + { -- | The token index. + _pltecTokenIndex :: !TokenIndex, + -- | The PLT configuration. + _pltecConfiguration :: !PLTConfiguration, + -- | The available energy. + _pltecEnergy :: !(Maybe Energy), + -- | The mutable token state. + _pltecMutableState :: !(MutableTokenState m) + } + +-- | PLT module execution error. +data PLTExecutionError fail + = -- | The PLT module run out of energy during execution. + PLTEOutOfEnergy + | -- | The PLT module encountered a runtime error during execution. + PLTEFail fail + +newtype KernelT fail ret m a = KernelT {runKernelT' :: ReaderT (PLTExecutionContext m) (ContT (Either fail ret) (StateT (PLTExecutionState m) m)) a} + deriving + ( Functor, + Applicative, + Monad, + MonadState (PLTExecutionState m), + MonadReader (PLTExecutionContext m) + ) + +runKernelT :: (Monad m) => KernelT fail a m a -> PLTExecutionContext m -> PLTExecutionState m -> m (Either fail a, PLTExecutionState m) +runKernelT a tokenIx = runStateT (runContT (runReaderT (runKernelT' a) tokenIx) (return . Right)) + +instance MonadTrans (KernelT fail ret) where + lift = KernelT . lift . lift . lift + +-- | The block state types for `KernelT fail ret m` are derived from the base monad `m`. +deriving via (MGSTrans (KernelT fail ret) m) instance BlockStateTypes (KernelT fail ret m) + +instance (BS.BlockStateOperations m, PVSupportsHaskellManagedPLT (MPV m)) => PLTKernelQuery (KernelT fail ret m) where + type PLTAccount (KernelT fail ret m) = (AccountIndex, AccountAddress) + getTokenState key = do + mutableState <- asks _pltecMutableState + lift $ BS.lookupTokenState key mutableState + getAccount addr = do + bs <- use plteBlockState + lift $ fmap ((,addr) . fst) <$> BS.bsoGetAccount bs addr + getAccountIndex (acctIndex, _acct) = return acctIndex + getAccountByIndex accountIndex = do + bs <- use plteBlockState + lift $ + BS.bsoGetAccountByIndex bs accountIndex >>= \case + Nothing -> return Nothing + Just account -> do + address <- BS.getAccountCanonicalAddress account + return $ Just (accountIndex, address) + getAccountBalance (acctIndex, _) = do + tokenIx <- asks _pltecTokenIndex + bs <- use plteBlockState + lift $ + BS.bsoGetAccountByIndex bs acctIndex >>= \case + Nothing -> error "getAccountBalance: Account does not exist" + Just acct -> BS.getAccountTokenBalance acct tokenIx + getAccountCanonicalAddress (acctIndex, _) = do + bs <- use plteBlockState + lift $ + BS.bsoGetAccountByIndex bs acctIndex >>= \case + Nothing -> error "getAccountCanonicalAddress: Account does not exist" + Just acct -> BS.getAccountCanonicalAddress acct + getCirculatingSupply = do + tokenIx <- asks _pltecTokenIndex + bs <- use plteBlockState + lift $ BS.getTokenCirculatingSupply bs tokenIx + getDecimals = asks (_pltDecimals . _pltecConfiguration) + +instance (BS.BlockStateOperations m, PVSupportsHaskellManagedPLT (MPV m)) => PLTKernelUpdate (KernelT fail ret m) where + setTokenState key mValue = do + plteStateIsDirty .= True + mutableState <- asks _pltecMutableState + lift $ BS.updateTokenState key mValue mutableState + + transfer (accIxFrom, accAddrFrom) (accIxTo, accAddrTo) amount mbMemo = do + context <- ask + let tokenIx = _pltecTokenIndex context + bs0 <- use plteBlockState + mbs2 <- lift $ do + mbs1 <- + BS.bsoUpdateTokenAccountBalance bs0 tokenIx accIxFrom $ + negativeTokenAmountDelta amount + case mbs1 of + Nothing -> return Nothing -- Sender has insufficient funds. + Just bs1 -> do + mbs2 <- + BS.bsoUpdateTokenAccountBalance bs1 tokenIx accIxTo $ + toTokenAmountDelta amount + return $ case mbs2 of + Nothing -> + -- This case cannot occur if the total supply is accurately + -- recorded, since it would imply that the total supply + -- exceeds the maximum representable amount. + error "Token kernel: transfer would overflow receiver balance" + Just bs2 -> + Just bs2 + case mbs2 of + Nothing -> return False + Just bs2 -> do + -- Log the transfer event. + plteEvents + %= ( TokenTransfer + { ettTokenId = _pltTokenId (_pltecConfiguration context), + ettFrom = HolderAccount accAddrFrom, + ettTo = HolderAccount accAddrTo, + ettAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)), + ettMemo = mbMemo, + ettFromLock = Nothing, + ettToLock = Nothing + } + : + ) + plteBlockState .= bs2 + return True + + logTokenEvent eventType eventDetails = do + tokenId <- asks (_pltTokenId . _pltecConfiguration) + plteEvents %= (TokenModuleEvent tokenId eventType eventDetails :) + + touch (accIx, _) = do + context <- ask + let tokenIx = _pltecTokenIndex context + bs0 <- use plteBlockState + mbBs1 <- lift $ BS.bsoTouchTokenAccount bs0 tokenIx accIx + case mbBs1 of + Nothing -> return False + Just bs1 -> do + plteBlockState .= bs1 + return True + +instance (BS.BlockStateOperations m, PVSupportsHaskellManagedPLT (MPV m)) => PLTKernelPrivilegedUpdate (KernelT fail ret m) where + mint (accIx, accAddr) amount = do + context <- ask + let tokenIx = _pltecTokenIndex context + bs <- use plteBlockState + currentSupply <- lift $ BS.getTokenCirculatingSupply bs tokenIx + if maxBound - amount < currentSupply + then return False -- Minting would overflow the circulating supply. + else do + bs' <- lift $ BS.bsoSetTokenCirculatingSupply bs tokenIx (currentSupply + amount) + mbNewBs <- + lift $ + BS.bsoUpdateTokenAccountBalance + bs' + tokenIx + accIx + (TokenAmountDelta (fromIntegral (theTokenRawAmount amount))) + case mbNewBs of + Nothing -> + -- This case cannot occur if the total supply is accurately + -- recorded, since it would imply that the total supply + -- exceeds the maximum representable amount. + error "Token kernel: mint would overflow receiver balance" + Just newBs -> do + -- Log the mint event. + plteEvents + %= ( TokenMint + { etmTokenId = _pltTokenId (_pltecConfiguration context), + etmTarget = HolderAccount accAddr, + etmAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)) + } + : + ) + plteBlockState .= newBs + return True + burn (accIx, accAddr) amount = do + context <- ask + let tokenIx = _pltecTokenIndex context + bs0 <- use plteBlockState + mbs1 <- lift $ BS.bsoUpdateTokenAccountBalance bs0 tokenIx accIx (negativeTokenAmountDelta amount) + case mbs1 of + Nothing -> return False + Just bs1 -> do + bs2 <- lift $ do + currentSupply <- BS.getTokenCirculatingSupply bs1 tokenIx + when (currentSupply < amount) $ + -- This case cannot occur if the total supply is accurately + -- recorded, since it would imply that the initial balance + -- on the target account exceeded to the total supply. + error "Token kernel: burn would underflow total supply" + BS.bsoSetTokenCirculatingSupply bs1 tokenIx (currentSupply - amount) + -- Log the burn event. + plteEvents + %= ( TokenBurn + { etbTokenId = _pltTokenId (_pltecConfiguration context), + etbTarget = HolderAccount accAddr, + etbAmount = TokenAmount amount (_pltDecimals (_pltecConfiguration context)) + } + : + ) + plteBlockState .= bs2 + return True + +instance + ( BS.BlockStateOperations m, + PVSupportsHaskellManagedPLT (MPV m) + ) => + PLTKernelChargeEnergy (KernelT (PLTExecutionError fail) ret m) + where + pltTickEnergy nrg = do + plteEnergyUsed += nrg + mbAvailableEnergy <- asks _pltecEnergy + case mbAvailableEnergy of + Nothing -> return () + Just availableEnergy -> do + energyUsed <- use plteEnergyUsed + unless (availableEnergy >= energyUsed) $ KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left PLTEOutOfEnergy) + +instance {-# OVERLAPPABLE #-} (Monad m, fail ~ fail') => (PLTKernelFail fail (KernelT fail' ret m)) where + -- To abort, we simply drop the continuation and return the error. + pltError err = KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left err) + +instance (Monad m) => (PLTKernelFail fail (KernelT (PLTExecutionError fail) ret m)) where + -- To abort, we simply drop the continuation and return the error. + pltError err = KernelT $ ReaderT $ \_ -> ContT $ \_ -> return (Left $ PLTEFail err) diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Module.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Module.hs index b72253e7de..79b139c3df 100644 --- a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Module.hs +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Module.hs @@ -5,7 +5,6 @@ module Concordium.Scheduler.ProtocolLevelTokens.Module where import Control.Monad import qualified Data.ByteString as BS -import qualified Data.ByteString.Builder as BS.Builder import qualified Data.ByteString.Lazy as LBS import qualified Data.Map.Strict as Map import Data.Maybe @@ -91,7 +90,7 @@ toTokenAmount decimals rawAmount = -- (if necessary) minting the initial supply to the token governance account. initializeToken :: (PLTKernelPrivilegedUpdate m, PLTKernelFail InitializeTokenError m, Monad m) => - TokenParameter -> + RawCbor -> m () initializeToken tokenParam = do case tokenInitializationParametersFromBytes tokenParamLBS of @@ -136,8 +135,7 @@ initializeToken tokenParam = do mintOK <- mint govAccount amt unless mintOK $ pltError (ITEInvalidMintAmount "Kernel failed to mint") where - tokenParamLBS = - BS.Builder.toLazyByteString $ BS.Builder.shortByteString $ parameterBytes tokenParam + tokenParamLBS = rawCborToLazyBytes tokenParam -- | A pre-processed token operation. This has all amounts converted to -- 'TokenRawAmount's and removes tags from memos. @@ -219,6 +217,7 @@ preprocessTokenUpdateTransaction decimals = mapM preproc . tokenOperations return PTOTokenRemoveDenyList{ptgoTarget = receiver} preproc TokenPause = return PTOTokenPause preproc TokenUnpause = return PTOTokenUnpause + preproc _ = pltError . encodeTokenRejectReason $ DeserializationFailure $ Just "token-operation: unsupported operation type" -- | Encode and log a 'TokenEvent'. logEncodeTokenEvent :: (PLTKernelUpdate m) => TokenEvent -> m () @@ -267,7 +266,7 @@ logEncodeTokenEvent te = logTokenEvent eventType details executeTokenUpdateTransaction :: (PLTKernelPrivilegedUpdate m, PLTKernelChargeEnergy m, PLTKernelFail EncodedTokenRejectReason m, Monad m) => TransactionContext m -> - TokenParameter -> + RawCbor -> m () executeTokenUpdateTransaction TransactionContext{..} tokenParam = do parsedTransaction <- case tokenUpdateTransactionFromBytes tokenParamLBS of @@ -300,7 +299,7 @@ executeTokenUpdateTransaction TransactionContext{..} tokenParam = do failTH OperationNotPermitted { trrOperationIndex = opIndex, - trrAddressNotPermitted = Just pthoRecipient, + trrAddressNotPermitted = Just $ accountTokenHolder $ chaAccount pthoRecipient, trrReason = Just "recipient not in allow list" } enforceDenyList <- isJust <$> getModuleState "denyList" @@ -321,7 +320,7 @@ executeTokenUpdateTransaction TransactionContext{..} tokenParam = do failTH OperationNotPermitted { trrOperationIndex = opIndex, - trrAddressNotPermitted = Just pthoRecipient, + trrAddressNotPermitted = Just $ accountTokenHolder $ chaAccount pthoRecipient, trrReason = Just "recipient in deny list" } success <- transfer tcSender recipientAccount pthoAmount pthoMemo @@ -424,8 +423,7 @@ executeTokenUpdateTransaction TransactionContext{..} tokenParam = do return (opIndex + 1) foldM_ handleOperation 0 operations where - tokenParamLBS = - BS.Builder.toLazyByteString $ BS.Builder.shortByteString $ parameterBytes tokenParam + tokenParamLBS = rawCborToLazyBytes tokenParam failTH = pltError . encodeTokenRejectReason checkPaused opIndex op = do paused <- isJust <$> getModuleState "paused" @@ -514,12 +512,14 @@ requireAccount :: -- | The account to check. CborAccountAddress -> m (PLTAccount m) -requireAccount trrOperationIndex holder@CborAccountAddress{..} = do - getAccount chaAccount >>= \case - Nothing -> - pltError . encodeTokenRejectReason $ - AddressNotFound{trrAddress = holder, ..} - Just acc -> return acc +requireAccount + trrOperationIndex + CborAccountAddress{..} = do + getAccount chaAccount >>= \case + Nothing -> + pltError . encodeTokenRejectReason $ + AddressNotFound{trrAddress = accountTokenHolder chaAccount, ..} + Just acc -> return acc -- | An error that may be when querying the token state. newtype QueryTokenError @@ -552,7 +552,7 @@ queryTokenModuleState = do getAccountByIndex govAccountIndex >>= \case Nothing -> pltError $ QTEInvariantViolation "Governance account does not exist" Just account -> return account - Just <$> accountTokenHolderShort <$> getAccountCanonicalAddress account + Just <$> accountTokenHolder <$> getAccountCanonicalAddress account tmsPaused <- Just . isJust <$> getModuleState "paused" tmsAllowList <- Just . isJust <$> getModuleState "allowList" tmsDenyList <- Just . isJust <$> getModuleState "denyList" diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Queries.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Queries.hs index 3fc373abca..ce1c0ff7e7 100644 --- a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Queries.hs +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/Queries.hs @@ -1,18 +1,31 @@ {-# LANGUAGE EmptyCase #-} +{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} -module Concordium.Scheduler.ProtocolLevelTokens.Queries where +module Concordium.Scheduler.ProtocolLevelTokens.Queries ( + QueryTokenModuleError (..), + QueryLockError (..), + SerializedLockId, + queryTokenInfo, + queryAccountTokens, + queryPLTList, + queryTokenAuthorizations, + queryLockList, + queryLockInfo, +) where import Control.Monad import Control.Monad.Cont import Control.Monad.Reader -import Data.Bool.Singletons +import Data.Functor import qualified Data.Map.Strict as Map import Data.Void import Concordium.Types +import qualified Concordium.Types.Locks as Locks +import qualified Concordium.Types.Queries.Locks as LockQueries import Concordium.Types.Queries.Tokens import qualified Concordium.GlobalState.BlockState as BS @@ -21,6 +34,7 @@ import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens (PLTConf import Concordium.GlobalState.Types import Concordium.Scheduler.ProtocolLevelTokens.Kernel import Concordium.Scheduler.ProtocolLevelTokens.Module +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Queries as RustQ -- | The 'QueryContext' provides the context to run 'PLTKernelQuery' operations against a -- particular token index and block state. @@ -62,7 +76,7 @@ runQueryTNoFail a ctx = instance MonadTrans (QueryT fail ret) where lift = QueryT . lift . lift -instance (BS.BlockStateQuery m, PVSupportsPLT (MPV m)) => PLTKernelQuery (QueryT fail ret m) where +instance (BS.BlockStateQuery m, PVSupportsHaskellManagedPLT (MPV m)) => PLTKernelQuery (QueryT fail ret m) where type PLTAccount (QueryT fail ret m) = IndexedAccount m getTokenState key = do QueryContext{..} <- ask @@ -90,16 +104,20 @@ instance (Monad m) => PLTKernelFail fail (QueryT fail ret m) where -- To abort, we simply drop the continuation and return the error. pltError err = QueryT $ ReaderT $ \_ -> ContT $ \_ -> return (Left err) --- | An error that may occur as a result of 'queryTokenInfo'. -data QueryTokenInfoError +-- | An error that may occur as a result of 'queryTokenInfo' or 'queryTokenAuthorizations'. +data QueryTokenModuleError = -- | The requested token does not exist in the block. - QTIEUnknownToken + QTMEUnknownToken | -- | An error occurred in the token module. - QTIEInternal !QueryTokenError + QTMEInternal !QueryTokenError + | -- | The protocol version does not support the query. + QTMEUnavailable + deriving (Eq) -instance Show QueryTokenInfoError where - show QTIEUnknownToken = "unknown token" - show (QTIEInternal e) = show e +instance Show QueryTokenModuleError where + show QTMEUnknownToken = "unknown token" + show (QTMEInternal e) = show e + show QTMEUnavailable = "The information is not available for this block" -- | Get the 'TokenInfo' associated with a 'TokenId' in the given 'BlockState'. queryTokenInfo :: @@ -107,20 +125,20 @@ queryTokenInfo :: (BS.BlockStateQuery m) => TokenId -> BlockState m -> - m (Either QueryTokenInfoError TokenInfo) -queryTokenInfo tokenId bs = case sSupportsPLT (accountVersion @(AccountVersionFor (MPV m))) of - SFalse -> return (Left QTIEUnknownToken) - STrue -> do + m (Either QueryTokenModuleError TokenInfo) +queryTokenInfo tokenId bs = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateNone -> return (Left QTMEUnknownToken) + SPLTStateV0 -> do mTokenIx <- BS.getTokenIndex bs tokenId case mTokenIx of - Nothing -> return (Left QTIEUnknownToken) + Nothing -> return (Left QTMEUnknownToken) Just tokenIx -> do PLTConfiguration{..} <- BS.getTokenConfiguration bs tokenIx totalSupply <- BS.getTokenCirculatingSupply bs tokenIx tokenState <- BS.getMutableTokenState bs tokenIx let ctx = QueryContext{qcTokenIndex = tokenIx, qcBlockState = bs, qcTokenState = tokenState} runQueryT queryTokenModuleState ctx >>= \case - Left e -> return (Left (QTIEInternal e)) + Left e -> return (Left (QTMEInternal e)) Right tms -> do let ts = TokenState @@ -130,27 +148,104 @@ queryTokenInfo tokenId bs = case sSupportsPLT (accountVersion @(AccountVersionFo tsModuleState = tms } return $ Right TokenInfo{tiTokenId = _pltTokenId, tiTokenState = ts} + SPLTStateV1 -> + RustQ.queryTokenInfo bs tokenId <&> \case + Just tokenInfo -> Right tokenInfo + Nothing -> Left QTMEUnknownToken + +-- | Get the 'TokenAuthorizations' associated with a 'TokenId' in the given 'BlockState'. +queryTokenAuthorizations :: + forall m. + (BS.BlockStateQuery m) => + TokenId -> + BlockState m -> + m (Either QueryTokenModuleError TokenAuthorizations) +queryTokenAuthorizations tokenId bs = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateNone -> return (Left QTMEUnknownToken) + SPLTStateV0 -> return (Left QTMEUnavailable) + SPLTStateV1 -> + RustQ.queryTokenAuthorizations bs tokenId <&> \case + Just out -> Right out + Nothing -> Left QTMEUnknownToken -- | Get the list of 'Token's on an account. -queryAccountTokens :: forall m. (PVSupportsPLT (MPV m), BS.BlockStateQuery m) => IndexedAccount m -> BlockState m -> m [Token] -queryAccountTokens acc bs = do - tokenStatesMap <- BS.getAccountTokens (snd acc) - forM (Map.toList tokenStatesMap) $ \(tokenIndex, tokenAccountState) -> do - pltConfiguration <- BS.getTokenConfiguration @_ @_ @m bs tokenIndex - let accountBalance = - TokenAmount - { taValue = tasBalance tokenAccountState, - taDecimals = _pltDecimals pltConfiguration - } - tokenState <- BS.getMutableTokenState bs tokenIndex - let ctx = QueryContext{qcTokenIndex = tokenIndex, qcBlockState = bs, qcTokenState = tokenState} - accountState <- runQueryTNoFail (queryAccountState acc) ctx - return - Token - { tokenId = _pltTokenId pltConfiguration, - tokenAccountState = - TokenAccountState - { balance = accountBalance, - moduleAccountState = accountState +queryAccountTokens :: + forall m. + (PVSupportsPLT (MPV m), BS.BlockStateQuery m) => + IndexedAccount m -> BlockState m -> m [Token] +queryAccountTokens acc bs = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateV0 -> + do + tokenStatesMap <- BS.getAccountTokens (snd acc) + forM (Map.toList tokenStatesMap) $ \(tokenIndex, tokenAccountState) -> do + pltConfiguration <- BS.getTokenConfiguration @_ @_ @m bs tokenIndex + let accountBalance = + TokenAmount + { taValue = tasBalance tokenAccountState, + taDecimals = _pltDecimals pltConfiguration + } + tokenState <- BS.getMutableTokenState bs tokenIndex + let ctx = QueryContext{qcTokenIndex = tokenIndex, qcBlockState = bs, qcTokenState = tokenState} + accountState <- runQueryTNoFail (queryAccountState acc) ctx + return + Token + { tokenId = _pltTokenId pltConfiguration, + tokenAccountState = + TokenAccountState + { balance = accountBalance, + moduleAccountState = accountState + } } - } + SPLTStateV1 -> + RustQ.queryTokenAccountInfos bs (fst acc) + +-- | Get the list all tokens +queryPLTList :: + forall m. + (BS.BlockStateQuery m) => + BlockState m -> + m [TokenId] +queryPLTList bs = + case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateNone -> return [] + SPLTStateV0 -> BS.getPLTList bs + SPLTStateV1 -> RustQ.queryPLTList bs + +-- | An error that may occur as a result of a lock query. +data QueryLockError + = -- | The requested lock does not exist in the block. + QLEUnknownLock + deriving (Eq) + +instance Show QueryLockError where + show QLEUnknownLock = "unknown lock" + +-- | Get the list of all PLT lock ids that exist in the given block. Pre-V1 PLT state versions +-- (including no PLT support) return the empty list. +queryLockList :: + forall m. + (BS.BlockStateQuery m) => + BlockState m -> + m [Locks.LockId] +queryLockList bs = + case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateNone -> return [] + SPLTStateV0 -> return [] + SPLTStateV1 -> RustQ.queryLockList bs + +type SerializedLockId = RustQ.SerializedLockId + +-- | Get the 'LockQueries.LockInfo' for a given lock ID. +queryLockInfo :: + forall m. + (BS.BlockStateQuery m) => + SerializedLockId -> + BlockState m -> + m (Either QueryLockError LockQueries.LockInfo) +queryLockInfo lockId bs = case sPltStateVersionFor (protocolVersion @(MPV m)) of + SPLTStateNone -> return (Left QLEUnknownLock) + SPLTStateV0 -> return (Left QLEUnknownLock) + SPLTStateV1 -> + RustQ.queryLockInfo bs lockId <&> \case + Just lockInfo -> Right lockInfo + Nothing -> Left QLEUnknownLock diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler.hs new file mode 100644 index 0000000000..44a483df4f --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler.hs @@ -0,0 +1,691 @@ +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Bindings into the Rust PLT Scheduler library. The module contains bindings to execute the payload of block items (currently for protocol-level tokens only). +-- Notice that block item headers are handled outside of the Rust PLT Scheduler. +-- +-- Each foreign imported function must match the signature of functions found on the Rust side. +module Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler ( + executeTransaction, + executeChainUpdate, +) where + +import Control.Monad +import Control.Monad.Except +import Control.Monad.Trans +import Data.Bool.Singletons (SBool (..)) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Unsafe as BS +import Data.Functor +import qualified Data.IORef as IORef +import qualified Data.Map.Strict as Map +import qualified Data.Serialize as S +import qualified Data.Text.Encoding as Text +import qualified Data.Word as Word +import qualified Foreign as FFI +import qualified Foreign.C.Types as FFI +import Lens.Micro + +import qualified Concordium.Types as Types +import qualified Concordium.Types.Execution as Types +import qualified Concordium.Types.Updates as Types +import qualified Concordium.Utils.Serialization as CS +import qualified Data.FixedByteString as FixedByteString + +import qualified Concordium.GlobalState.BlockState as BS +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.ExternalChainParameters as ECP +import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState as PLTBlockState +import qualified Concordium.GlobalState.Types as BS +import qualified Concordium.Scheduler.Environment as EI +import Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.BlockStateCallbacks +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Memory as Memory +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Status as Status + +-- | Execute a transaction payload in the 'SchedulerMonad' modifying the block state accordingly. The transaction +-- is executed via the Rust PLT Scheduler library. Only 'TokenUpdate' transaction payloads are currently supported. +executeTransaction :: + forall m. + (BS.BlockStateOperations m, Types.PVSupportsRustManagedPLT (Types.MPV m)) => + EI.WithDepositContext m -> + -- | Transaction payload. + Types.Payload -> + -- | Transaction summary + EI.SchedulerT m (Maybe (Types.TransactionSummary (Types.TransactionOutcomesVersionFor (Types.MPV m)))) +executeTransaction depositContext payload = do + Types.ChainMetadata{..} <- EI.getChainMetadata + executeTransactionWithTimestamp slotTime depositContext payload + +-- | Execute a transaction payload in the 'SchedulerMonad' modifying the block state accordingly. The transaction +-- is executed via the Rust PLT Scheduler library. Only 'TokenUpdate' transaction payloads are currently supported. +executeTransactionWithTimestamp :: + forall m. + (BS.BlockStateOperations m, Types.PVSupportsRustManagedPLT (Types.MPV m)) => + -- | Timestamp of the block in which the transaction occurs. + Types.Timestamp -> + EI.WithDepositContext m -> + -- | Transaction payload. + Types.Payload -> + -- | Transaction summary + EI.SchedulerT m (Maybe (Types.TransactionSummary (Types.TransactionOutcomesVersionFor (Types.MPV m)))) +executeTransactionWithTimestamp blockTimestamp depositContext tokenUpdate = do + EI.withDeposit depositContext executeTransactionInLocalT EI.defaultSuccess + where + executeTransactionInLocalT :: EI.LocalT [Types.Event] (EI.SchedulerT m) [Types.Event] + executeTransactionInLocalT = do + (remainingEnergy, _energyLimitReason) <- EI.getEnergy + + summary <- lift $ executeTransactionInLocalTLiftedSchedulerMonad remainingEnergy + + -- Charge energy + EI.tickEnergy $ tesUsedEnergy summary + + -- Map execution outcome. + case tesOutcome summary of + TransactionExecutionOutcomeSuccess (TransactionExecutionSuccess () events) -> do + return events + TransactionExecutionOutcomeReject (TransactionExecutionReject rejectReason) -> + EI.rejectTransaction rejectReason + + executeTransactionInLocalTLiftedSchedulerMonad :: Types.Energy -> EI.SchedulerT m (TransactionExecutionSummary ()) + executeTransactionInLocalTLiftedSchedulerMonad remainingEnergy = + -- We need to run with block state rollback, since callbacks may modify the block state, and + -- it is modified in a non-functional way via interior mutability (PersistentBlockState is an IORef). + EI.withBlockStateRollback $ + do + -- Get current block state. + blockState0 <- EI.getBlockState + + -- Execute transaction in the block state monad. + summary <- lift $ executeTransactionInBSOMonad remainingEnergy blockState0 + + -- Set updated block state if operation was successful and set rollback. + rollback <- case tesOutcome summary of + TransactionExecutionOutcomeSuccess (TransactionExecutionSuccess blockState1 _) -> do + EI.setBlockState blockState1 + return False + TransactionExecutionOutcomeReject (TransactionExecutionReject _) -> + return True + + return $ + ( summary $> (), + rollback + ) + + -- Execute a transaction with the given block state as input. + -- Returns the updated block state and events produced if successful, otherwise a failure kind. + -- + -- NOTICE: The caller must ensure to rollback state changes applied via callbacks in case a failure kind is returned. + executeTransactionInBSOMonad :: + Types.Energy -> + BS.UpdatableBlockState m -> + m (TransactionExecutionSummary (BS.UpdatableBlockState m)) + executeTransactionInBSOMonad remainingEnergy blockState0 = do + -- Get current PLT block state and external chain parameters when supported. + pltBlockState0 <- BS.bsoGetRustPLTBlockState blockState0 + externalChainParameters <- + ( case Types.sSupportsRustManagedECP (Types.protocolVersion @(Types.MPV m)) of + SFalse -> return Nothing + STrue -> Just <$> BS.bsoGetExternalChainParameters blockState0 + ) :: + m (Maybe (ECP.ForeignExternalChainParametersPtr (Types.MPV m))) + + -- Put block state in an IORef to allow callbacks to update it. + blockStateIORef <- BS.liftBlobStore $ liftIO $ IORef.newIORef blockState0 + queryCallbacks <- unliftBlockStateQueryCallbacks blockStateIORef + operationCallbacks <- unliftBlockStateOperationCallbacks blockStateIORef + + -- Execute chain update via FFI. + outcome <- + BS.liftBlobStore $ + executeTransactionInBlobStoreMonad + (Types.protocolVersion @(Types.MPV m)) + pltBlockState0 + externalChainParameters + queryCallbacks + operationCallbacks + (fst $ depositContext ^. EI.wtcSenderAccount) + (depositContext ^. EI.wtcSenderAddress) + remainingEnergy + + -- Get block state from IORef and set the updated PLT block state if operation was successful. + forM outcome $ \pltBlockState1 -> do + blockState1 <- BS.liftBlobStore $ liftIO $ IORef.readIORef blockStateIORef + BS.bsoSetRustPLTBlockState blockState1 pltBlockState1 + + -- Execute a transaction payload with the given PLT block state as input. + -- Returns the updated PLT block state and events produced if successful, otherwise a failure kind. + -- + -- NOTICE: The caller must ensure to rollback state changes applied via callbacks in case of the transaction being rejected. + executeTransactionInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block protocol version + Types.SProtocolVersion pv -> + -- Block state to mutate. + PLTBlockState.ForeignPLTBlockStatePtr pv -> + -- Node-owned external chain parameters, present only for P11. + Maybe (ECP.ForeignExternalChainParametersPtr pv) -> + -- Callbacks need for block state queries on the state maintained by Haskell. + BlockStateQueryCallbacks -> + -- Callbacks need for block state operations on the state maintained by Haskell. + BlockStateOperationCallbacks -> + -- The account index of the account which signed as the sender of the transaction. + Types.AccountIndex -> + -- The account address of the account which signed as the sender of the transaction. + Types.AccountAddress -> + -- Remaining energy. + Types.Energy -> + -- Outcome of the execution + m' (TransactionExecutionSummary (PLTBlockState.ForeignPLTBlockStatePtr pv)) + executeTransactionInBlobStoreMonad + spv + blockState + externalChainParameters + queryCallbacks + operationCallbacks + senderAccountIndex + (Types.AccountAddress senderAccountAddress) + remainingEnergy = + do + let transactionPayloadByteString = S.runPut $ Types.putPayload tokenUpdate + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \usedEnergyOutPtr -> FFI.alloca $ \resultingBlockStateOutPtr -> + FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + updateTokenAccountBalanceCallbackPtr <- wrapUpdateTokenAccountBalance $ updateTokenAccountBalance operationCallbacks + touchTokenAccountCallbackPtr <- wrapTouchTokenAccount $ touchTokenAccount operationCallbacks + incrementPltUpdateSequenceCallbackPtr <- wrapIncrementPltUpdateSequenceNumber $ incrementPltUpdateSequenceNumber operationCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState blockState $ \blockStatePtr -> + let withExternalChainParameters = + maybe + (\use -> use FFI.nullPtr) + (\params use -> ECP.withExternalChainParameters params use) + externalChainParameters + in withExternalChainParameters $ \externalChainParametersPtr -> + FixedByteString.withPtrReadOnly (senderAccountAddress) $ \senderAccountAddressPtr -> + BS.unsafeUseAsCStringLen transactionPayloadByteString $ \(transactionPayloadPtr, transactionPayloadLen) -> + ffiExecuteTransaction + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + updateTokenAccountBalanceCallbackPtr + touchTokenAccountCallbackPtr + incrementPltUpdateSequenceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + blockStatePtr + externalChainParametersPtr + (FFI.castPtr transactionPayloadPtr) + (fromIntegral transactionPayloadLen) + (fromIntegral senderAccountIndex) + senderAccountAddressPtr + (EI._wtcTransactionSequenceNumber depositContext) + blockTimestamp + (fromIntegral remainingEnergy) + resultingBlockStateOutPtr + usedEnergyOutPtr + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr updateTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr touchTokenAccountCallbackPtr + FFI.freeHaskellFunPtr incrementPltUpdateSequenceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returend via out pointers + usedEnergy <- fromIntegral <$> FFI.peek usedEnergyOutPtr + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + oucome <- case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + updatedBlockState <- FFI.peek resultingBlockStateOutPtr >>= PLTBlockState.wrapFFIPtr + let getEvents = S.isolate (BS.length returnData) $ CS.getListOf $ Types.getEvent spv + let events = + either + (\message -> error $ "Transaction events from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getEvents returnData + return $ + TransactionExecutionOutcomeSuccess $ + TransactionExecutionSuccess + { tesUpdatedBlockState = updatedBlockState, + tesEvents = events + } + Just Status.FSCFailed -> do + let getRejectReason = S.isolate (BS.length returnData) S.get + let rejectReason = + either + (\message -> error $ "Transaction reject reason from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getRejectReason returnData + return $ + TransactionExecutionOutcomeReject $ + TransactionExecutionReject + { terRejectReason = rejectReason + } + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiExecuteTransaction' resulted in panic with message: " ++ show message) + Nothing -> error ("Unexpected status code from calling 'ffiExecuteTransaction': " ++ show statusCode) + return + TransactionExecutionSummary + { tesUsedEnergy = usedEnergy, + tesOutcome = oucome + } + +-- | C-binding for calling the Rust function `plt_scheduler::scheduler::execute_transaction`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: Transaction execution succeeded and transaction was applied to block state. +-- - `1`: Transaction was rejected with a reject reason. Block state changes applied +-- via callbacks must be rolled back. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_execute_transaction" + ffiExecuteTransaction :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to update the token account balance in the haskell-managed block state. + UpdateTokenAccountBalanceCallbackPtr -> + -- | Called to touch token account state in the haskell-managed block state. + TouchTokenAccountCallbackPtr -> + -- | Called to increment the PLT update sequence number. + IncrementPltUpdateSequenceNumberCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Read-only external chain parameters, or null before P11. + FFI.Ptr ECP.RustExternalChainParameters -> + -- | Pointer to transaction payload bytes. + FFI.Ptr Word.Word8 -> + -- | Byte length of transaction payload. + FFI.CSize -> + -- | The account index of the account which signed as the sender of the transaction. + Word.Word64 -> + -- | Pointer to 32 bytes representing the account address of the account which signed as the + -- sender of the transaction. + FFI.Ptr Word.Word8 -> + -- | Transaction sequence number. + Types.Nonce -> + -- | Block timestamp at which the transaction is executed. + Types.Timestamp -> + -- | Remaining energy + Word.Word64 -> + -- | Output location for the resulting PLT block state. + -- This is only set if the execution was successful (return code `0`) + FFI.Ptr (FFI.Ptr PLTBlockState.RustPLTBlockState) -> + -- | Output location for the energy used by the execution. + -- This is written regardless of whether return code is `0` or `1` + FFI.Ptr Word.Word64 -> + -- | Output location for array containing return data, which is either serialized events or reject reason. + -- If the return value is `0`, the data is a list of transaction events. If the return value is `1`, it is a reject reason. + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0` if transaction was executed and applied successfully. + -- * `1` if transaction was rejected. Block state changes applied + -- via callbacks must be rolled back. + IO Word.Word8 + +-- | Execute a chain update in the 'SchedulerMonad' modifying the block state accordingly. The chain update +-- is executed via the Rust PLT Scheduler library. Only 'CratePLT' chain updates are currently supported. +executeChainUpdate :: + forall m. + (BS.BlockStateOperations m, Types.PVSupportsRustManagedPLT (Types.MPV m)) => + -- | Chain update header. + Types.UpdateHeader -> + -- | Chain update payload. + Types.CreatePLT -> + -- | Failure or events. + EI.SchedulerT m (Either Types.FailureKind Types.ValidResult) +executeChainUpdate updateHeader createPLT = + fmap join $ runExceptT $ do + unless (Types.updateEffectiveTime updateHeader == 0) $ throwError Types.InvalidUpdateTime + -- We need to run with block state rollback, since callbacks may modify the block state, and + -- it is modified in a non-functional way via interior mutability (PersistentBlockState is an IORef). + lift $ EI.withBlockStateRollback $ do + -- Get current block state. + blockState0 <- EI.getBlockState + + -- Execute chain update in the block state monad. + outcome <- lift $ executeChainUpdateInBSOMonad blockState0 + + -- Set updated block state if operation was successful and map outcome to return value. + case outcome of + ChainUpdateExecutionOutcomeSuccess (ChainUpdateExecutionSuccess blockState1 events) -> do + EI.setBlockState blockState1 + return (Right $ Types.TxSuccess events, False) + ChainUpdateExecutionOutcomeFailed (ChainUpdateExecutionFailed failureKind) -> + return (Left failureKind, True) + where + -- Execute a chain update with the given block state as input. + -- Returns the updated block state and events produced if successful, otherwise a failure kind. + -- + -- NOTICE: The caller must ensure to rollback state changes applied via callbacks in case a failure kind is returned. + executeChainUpdateInBSOMonad :: + BS.UpdatableBlockState m -> m (ChainUpdateExecutionOutcome (BS.UpdatableBlockState m)) + executeChainUpdateInBSOMonad blockState0 = do + -- Get current PLT block state + pltBlockState0 <- BS.bsoGetRustPLTBlockState blockState0 + + -- Put block state in an IORef to allow callbacks to update it. + blockStateIORef <- BS.liftBlobStore $ liftIO $ IORef.newIORef blockState0 + queryCallbacks <- unliftBlockStateQueryCallbacks blockStateIORef + operationCallbacks <- unliftBlockStateOperationCallbacks blockStateIORef + + -- Execute chain update via FFI. + outcome <- + BS.liftBlobStore $ + executeChainUpdateInBlobStoreMonad + (Types.protocolVersion @(Types.MPV m)) + pltBlockState0 + queryCallbacks + operationCallbacks + + -- Get block state from IORef and set the updated PLT block state if operation was successful. + forM outcome $ \pltBlockState1 -> do + blockState1 <- BS.liftBlobStore $ liftIO $ IORef.readIORef blockStateIORef + BS.bsoSetRustPLTBlockState blockState1 pltBlockState1 + + -- Execute a chain update with the given PLT block state as input. + -- Returns the updated PLT block state and events produced if successful, otherwise a failure kind. + -- The function is a wrapper around an FFI call to the Rust PLT Scheduler library. + -- + -- NOTICE: The caller must ensure to rollback state changes applied via callbacks in case a failure kind is returned. + executeChainUpdateInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block protocol version + Types.SProtocolVersion pv -> + -- Block state to mutate. + PLTBlockState.ForeignPLTBlockStatePtr pv -> + -- Callbacks need for block state queries on the state maintained by Haskell. + BlockStateQueryCallbacks -> + -- Callbacks need for block state operations on the state maintained by Haskell. + BlockStateOperationCallbacks -> + -- Outcome of the execution + m' (ChainUpdateExecutionOutcome (PLTBlockState.ForeignPLTBlockStatePtr pv)) + executeChainUpdateInBlobStoreMonad + spv + blockState + queryCallbacks + operationCallbacks = + do + let chainUpdatePayloadByteString = S.runPut $ Types.putUpdatePayload $ Types.CreatePLTUpdatePayload createPLT + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \resultingBlockStateOutPtr -> + FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + updateTokenAccountBalanceCallbackPtr <- wrapUpdateTokenAccountBalance $ updateTokenAccountBalance operationCallbacks + touchTokenAccountCallbackPtr <- wrapTouchTokenAccount $ touchTokenAccount operationCallbacks + incrementPltUpdateSequenceCallbackPtr <- wrapIncrementPltUpdateSequenceNumber $ incrementPltUpdateSequenceNumber operationCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState blockState $ \blockStatePtr -> + BS.unsafeUseAsCStringLen chainUpdatePayloadByteString $ \(chainUpdatePayloadPtr, chainUpdatePayloadLen) -> + ffiExecuteChainUpdate + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + updateTokenAccountBalanceCallbackPtr + touchTokenAccountCallbackPtr + incrementPltUpdateSequenceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + blockStatePtr + (FFI.castPtr chainUpdatePayloadPtr) + (fromIntegral chainUpdatePayloadLen) + resultingBlockStateOutPtr + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr updateTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr touchTokenAccountCallbackPtr + FFI.freeHaskellFunPtr incrementPltUpdateSequenceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returned via out pointers + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + updatedBlockState <- FFI.peek resultingBlockStateOutPtr >>= PLTBlockState.wrapFFIPtr + let getEvents = S.isolate (BS.length returnData) $ CS.getListOf $ Types.getEvent spv + let events = + either + (\message -> error $ "Chain update events from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getEvents returnData + return $ + ChainUpdateExecutionOutcomeSuccess $ + ChainUpdateExecutionSuccess + { cuesUpdatedBlockState = updatedBlockState, + cuesEvents = events + } + Just Status.FSCFailed -> do + let getFailureKind = S.isolate (BS.length returnData) S.get + let failureKind = + either + (\message -> error $ "Chain update failure kind from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getFailureKind returnData + return $ + ChainUpdateExecutionOutcomeFailed $ + ChainUpdateExecutionFailed + { cuefFailureKind = failureKind + } + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiExecuteChainUpdate' resulted in panic with message: " ++ show message) + Nothing -> error ("Unexpected status code from calling 'ffiExecuteChainUpdate': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::scheduler::execute_chain_update`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: Chain update execution succeeded and update was applied to block state. +-- - `1`: Chain update failed. Block state changes applied +-- via callbacks must be rolled back. +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_execute_chain_update" + ffiExecuteChainUpdate :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to update the token account balance in the haskell-managed block state. + UpdateTokenAccountBalanceCallbackPtr -> + -- | Called to touch token account state in the haskell-managed block state. + TouchTokenAccountCallbackPtr -> + -- | Called to increment the PLT update sequence number. + IncrementPltUpdateSequenceNumberCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Pointer to chain update payload bytes. + FFI.Ptr Word.Word8 -> + -- | Byte length of chain update payload. + FFI.CSize -> + -- | Output location for the resulting PLT block state. + -- Only written set if the execution was successful (return code `0`) + FFI.Ptr (FFI.Ptr PLTBlockState.RustPLTBlockState) -> + -- | Output location for array containing return data, which is either serialized events or failure kind. + -- If the return value is `0`, the data is a list of events. If the return value is `1`, it is a failure kind. + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0` if chain update was executed and applied successfully. + -- * `1` if chain update failed. Block state changes applied + -- via callbacks must be rolled back. + IO Word.Word8 + +-- | Summary of executing a transaction using the PLT scheduler. +data TransactionExecutionSummary a = TransactionExecutionSummary + { -- | The amount of energy used by the transaction execution. + tesUsedEnergy :: Types.Energy, + -- | The outcome (success/rejection) of the transaction execution. The transaction can either be successful or rejected. + -- If the transaction is rejected, the changes to the block state must be rolled back. + tesOutcome :: TransactionExecutionOutcome a + } + deriving (Functor, Foldable, Traversable) + +-- | Outcome of the transaction: successful or rejected. +-- If the transaction was rejected, the changes to the block state must be rolled back. +data TransactionExecutionOutcome a + = TransactionExecutionOutcomeSuccess (TransactionExecutionSuccess a) + | TransactionExecutionOutcomeReject TransactionExecutionReject + deriving (Functor, Foldable, Traversable) + +-- | Representation of rejected transaction execution outcome +data TransactionExecutionReject = TransactionExecutionReject + { -- | Transaction reject reason + terRejectReason :: Types.RejectReason + } + +-- | Representation of successful transaction execution outcome +data TransactionExecutionSuccess a = TransactionExecutionSuccess + { -- | The updated block state after the execution + tesUpdatedBlockState :: a, + -- | Events produced during the execution + tesEvents :: [Types.Event] + } + deriving (Functor, Foldable, Traversable) + +-- | Outcome of the chain update: successful or failed. +-- If the chain update failed, the changes to the block state must be rolled back. +data ChainUpdateExecutionOutcome a + = ChainUpdateExecutionOutcomeSuccess (ChainUpdateExecutionSuccess a) + | ChainUpdateExecutionOutcomeFailed ChainUpdateExecutionFailed + deriving (Functor, Foldable, Traversable) + +-- | Representation of failed chain update outcome +data ChainUpdateExecutionFailed = ChainUpdateExecutionFailed + { -- | Chain update failure kind + cuefFailureKind :: Types.FailureKind + } + +-- | Representation of successful chain update outcome +data ChainUpdateExecutionSuccess a = ChainUpdateExecutionSuccess + { -- | The updated block state after the execution + cuesUpdatedBlockState :: a, + -- | Events produced during the execution + cuesEvents :: [Types.Event] + } + deriving (Functor, Foldable, Traversable) + +-- | "Unlifts" the callback queries from the 'BlockStateOperations' monad into the IO monad, such that they can +-- be converted to FFI function pointers. +unliftBlockStateQueryCallbacks :: + forall m. + (Types.PVSupportsPLT (Types.MPV m), BS.BlockStateOperations m) => + IORef.IORef (BS.UpdatableBlockState m) -> + m BlockStateQueryCallbacks +unliftBlockStateQueryCallbacks bsIORef = BS.withUnliftBSO $ \unlift -> + do + let readTokenAccountBalance accountIndex tokenIndex = withIORef bsIORef $ \bs -> do + maybeAccount <- unlift $ BS.bsoGetAccountByIndex bs accountIndex + let account = maybe (error $ "Account with index does not exist: " ++ show accountIndex) id maybeAccount + unlift $ BS.getAccountTokenBalance account tokenIndex + getAccountIndexByAddress accountAddress = withIORef bsIORef $ \bs -> do + maybeAccount <- unlift $ BS.bsoGetAccount bs accountAddress + return $ fst <$> maybeAccount + getAccountAddressByIndex accountIndex = withIORef bsIORef $ \bs -> do + maybeAccount <- unlift $ BS.bsoGetAccountByIndex bs accountIndex + forM maybeAccount $ \account -> unlift $ BS.getAccountCanonicalAddress account + getTokenAccountStates accountIndex = withIORef bsIORef $ \bs -> do + maybeAccount <- unlift $ BS.bsoGetAccountByIndex bs accountIndex + let account = maybe (error $ "Account with index does not exist: " ++ show accountIndex) id maybeAccount + fmap Map.toList $ unlift $ BS.getAccountTokens account + + return BlockStateQueryCallbacks{..} + +-- | "Unlifts" the callback operations from the 'BlockStateOperations' monad into the IO monad, such that they can +-- be converted to FFI function pointers. +unliftBlockStateOperationCallbacks :: + forall m. + (Types.PVSupportsPLT (Types.MPV m), BS.BlockStateOperations m) => + IORef.IORef (BS.UpdatableBlockState m) -> + m BlockStateOperationCallbacks +unliftBlockStateOperationCallbacks bsIORef = BS.withUnliftBSO $ \unlift -> + do + let updateTokenAccountBalance accountIndex tokenIndex tokenAmountDelta = + modifyIORef bsIORef $ \bs -> do + maybeBs1 <- unlift $ BS.bsoUpdateTokenAccountBalance bs tokenIndex accountIndex tokenAmountDelta + return $ case maybeBs1 of + Just bs1 -> (bs1, Just ()) + Nothing -> (bs, Nothing) + touchTokenAccount accountIndex tokenIndex = + modifyIORef_ bsIORef $ \bs -> do + maybeBs1 <- unlift $ BS.bsoTouchTokenAccount bs tokenIndex accountIndex + return $ case maybeBs1 of + Just bs1 -> bs1 + Nothing -> bs + incrementPltUpdateSequenceNumber = + modifyIORef_ bsIORef $ \bs -> do + unlift $ BS.bsoIncrementPLTUpdateSequenceNumber bs + + return BlockStateOperationCallbacks{..} + +withIORef :: IORef.IORef a -> (a -> IO b) -> IO b +withIORef ref f = IORef.readIORef ref >>= f + +modifyIORef :: IORef.IORef a -> (a -> IO (a, b)) -> IO b +modifyIORef ref f = do + (val, ret) <- IORef.readIORef ref >>= f + IORef.writeIORef ref val + return ret + +modifyIORef_ :: IORef.IORef a -> (a -> IO a) -> IO () +modifyIORef_ ref f = modifyIORef ref (fmap (,()) . f) diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/BlockStateCallbacks.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/BlockStateCallbacks.hs new file mode 100644 index 0000000000..e151265c34 --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/BlockStateCallbacks.hs @@ -0,0 +1,359 @@ +{-# LANGUAGE TypeApplications #-} + +-- | Bindings for calling back from the Rust PLT Scheduler to the Haskell mainained part of the block state. +-- +-- Each foreign function definition must match the definitions of functions found on the Rust side. +module Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.BlockStateCallbacks ( + BlockStateQueryCallbacks (..), + BlockStateOperationCallbacks (..), + ReadTokenAccountBalance, + ReadTokenAccountBalanceCallbackPtr, + wrapReadTokenAccountBalance, + UpdateTokenAccountBalance, + UpdateTokenAccountBalanceCallbackPtr, + wrapUpdateTokenAccountBalance, + TouchTokenAccount, + TouchTokenAccountCallbackPtr, + wrapTouchTokenAccount, + IncrementPltUpdateSequenceNumber, + IncrementPltUpdateSequenceNumberCallbackPtr, + wrapIncrementPltUpdateSequenceNumber, + GetAccountIndexByAddress, + GetAccountIndexByAddressCallbackPtr, + wrapGetAccountIndexByAddress, + GetAccountAddressByIndex, + GetAccountAddressByIndexCallbackPtr, + wrapGetAccountAddressByIndex, + GetTokenAccountStates, + GetTokenAccountStatesCallbackPtr, + wrapGetTokenAccountStates, +) where + +import qualified Data.ByteString.Unsafe as BS +import Data.Maybe +import qualified Data.Serialize as S +import qualified Data.Word as Word +import qualified Foreign as FFI + +import qualified Concordium.ID.Types as Types +import qualified Concordium.Types as Types +import qualified Concordium.Types.Tokens as Tokens +import qualified Concordium.Utils.Serialization as CS + +import qualified Concordium.GlobalState.ContractStateFFIHelpers as ContractStateFFI +import qualified Concordium.GlobalState.Persistent.Account.ProtocolLevelTokens as AccountTokens +import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens as Tokens + +-- | Block state query callbacks. These are used by the Rust PLT Scheduler library to +-- query the part of the block state that is maintained by Haskell. +data BlockStateQueryCallbacks = BlockStateQueryCallbacks + { readTokenAccountBalance :: ReadTokenAccountBalance, + getAccountIndexByAddress :: GetAccountIndexByAddress, + getAccountAddressByIndex :: GetAccountAddressByIndex, + getTokenAccountStates :: GetTokenAccountStates + } + +-- | Block state operation callbacks. These are used by the Rust PLT Scheduler library to +-- perform operations on the part of the block state that is maintained by Haskell. +data BlockStateOperationCallbacks = BlockStateOperationCallbacks + { updateTokenAccountBalance :: UpdateTokenAccountBalance, + touchTokenAccount :: TouchTokenAccount, + incrementPltUpdateSequenceNumber :: IncrementPltUpdateSequenceNumber + } + +-- | Callback function for reading a token account balance. +type ReadTokenAccountBalance = + -- | Index of the account to read a token balance for. The account must exist. + Types.AccountIndex -> + -- | Index of the token to read the balance of. The token must exist. + Tokens.TokenIndex -> + -- | The balance. + IO Tokens.TokenRawAmount + +-- | Internal helper function for mapping the 'ReadTokenAccountBalance' into the more +-- low-level function pointer which can be passed in FFI. +wrapReadTokenAccountBalance :: ReadTokenAccountBalance -> IO ReadTokenAccountBalanceCallbackPtr +wrapReadTokenAccountBalance func = + ffiWrapReadTokenAccountBalanceCallback callback + where + callback :: ReadTokenAccountBalanceCallbackFFI + callback accountIndex tokenIndex = do + amount <- func (fromIntegral accountIndex) (fromIntegral tokenIndex) + return $ Tokens.theTokenRawAmount amount + +-- | Callback function for reading a token account balance. +-- +-- This is passed as a function pointer in FFI to call, see also 'ReadTokenAccountBalanceCallback' +-- for the more type-safe variant. +type ReadTokenAccountBalanceCallbackFFI = + -- | Index of the account to read a token balance for. The account must exist. + Word.Word64 -> + -- | Index of the token to read the balance of. The token must exist. + Word.Word64 -> + -- | The balanace. + IO Word.Word64 + +-- | The callback function pointer type for reading a token account balance. +type ReadTokenAccountBalanceCallbackPtr = FFI.FunPtr ReadTokenAccountBalanceCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapReadTokenAccountBalanceCallback :: + ReadTokenAccountBalanceCallbackFFI -> IO ReadTokenAccountBalanceCallbackPtr + +-- | Callback function for updating a token account balance. +type UpdateTokenAccountBalance = + -- | Index of the account to update a token balance for. + Types.AccountIndex -> + -- | Index of the token to update the balance of. + Tokens.TokenIndex -> + -- | The change to account balance. + AccountTokens.TokenAmountDelta -> + -- | Status code, where 'Nothing' represents a balance overflow. + IO (Maybe ()) + +-- | Internal helper function for mapping the 'UpdateTokenAccountBalance' into the more +-- low-level function pointer which can be passed in FFI. +wrapUpdateTokenAccountBalance :: UpdateTokenAccountBalance -> IO UpdateTokenAccountBalanceCallbackPtr +wrapUpdateTokenAccountBalance func = + ffiWrapUpdateTokenAccountBalanceCallback callback + where + callback :: UpdateTokenAccountBalanceCallbackFFI + callback accountIndex tokenIndex amount addAmount = do + let amountDelta = + case addAmount of + 0 -> AccountTokens.TokenAmountDelta $ -fromIntegral amount + 1 -> AccountTokens.TokenAmountDelta $ fromIntegral amount + _ -> error ("Boolean argument must be 0 or 1, was " ++ (show addAmount)) + result <- func (fromIntegral accountIndex) (fromIntegral tokenIndex) amountDelta + return $ if isJust result then 0 else 1 + +-- | Callback function for updating a token account balance. +-- +-- This is passed as a function pointer in FFI to call, see also 'UpdateTokenAccountBalance' +-- for the more type-safe variant. +type UpdateTokenAccountBalanceCallbackFFI = + -- | Index of the account to update a token balance for. + Word.Word64 -> + -- | Index of the token to update the balance of. + Word.Word64 -> + -- | The token amount to add to or subtract from the balance. + Word.Word64 -> + -- | If 1, add the amount to the balance. If 0, subtract the amount from the balance. + Word.Word8 -> + -- | Status code, where non-null represents a balance overflow. + IO Word.Word8 + +-- | The callback function pointer type for updating a token account balance. +type UpdateTokenAccountBalanceCallbackPtr = FFI.FunPtr UpdateTokenAccountBalanceCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapUpdateTokenAccountBalanceCallback :: + UpdateTokenAccountBalanceCallbackFFI -> IO UpdateTokenAccountBalanceCallbackPtr + +-- | Callback function for touching token account state. +type TouchTokenAccount = + -- | Index of the account to touch token state for. + Types.AccountIndex -> + -- | Index of the token to touch in account state. + Tokens.TokenIndex -> + IO () + +-- | Internal helper function for mapping the 'TouchTokenAccount' into the more +-- low-level function pointer which can be passed in FFI. +wrapTouchTokenAccount :: TouchTokenAccount -> IO TouchTokenAccountCallbackPtr +wrapTouchTokenAccount func = + ffiWrapTouchTokenAccountCallback callback + where + callback :: TouchTokenAccountCallbackFFI + callback accountIndex tokenIndex = do + func (fromIntegral accountIndex) (fromIntegral tokenIndex) + +-- | Callback function for updating a token account balance. +-- +-- This is passed as a function pointer in FFI to call, see also 'TouchTokenAccount' +-- for the more type-safe variant. +type TouchTokenAccountCallbackFFI = + -- | Index of the account to touch token state for. + Word.Word64 -> + -- | Index of the token to touch in account state. + Word.Word64 -> + IO () + +-- | The callback function pointer type for touching token account state. +type TouchTokenAccountCallbackPtr = FFI.FunPtr TouchTokenAccountCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapTouchTokenAccountCallback :: + TouchTokenAccountCallbackFFI -> IO TouchTokenAccountCallbackPtr + +-- | Callback function for incrementing the PLT update sequence number. +type IncrementPltUpdateSequenceNumber = + IO () + +-- | Internal helper function for mapping the 'IncrementPltUpdateSequenceNumber' into the more +-- low-level function pointer which can be passed in FFI. +wrapIncrementPltUpdateSequenceNumber :: IncrementPltUpdateSequenceNumber -> IO IncrementPltUpdateSequenceNumberCallbackPtr +wrapIncrementPltUpdateSequenceNumber func = + ffiWrapIncrementPltUpdateSequenceNumberCallback callback + where + callback :: IncrementPltUpdateSequenceNumberCallbackFFI + callback = func + +-- | Callback function for incrementing the PLT update sequence number. +-- +-- This is passed as a function pointer in FFI to call, see also 'IncrementPltUpdateSequenceNumber' +-- for the more type-safe variant. +type IncrementPltUpdateSequenceNumberCallbackFFI = + IO () + +-- | The callback function pointer type for incrementing the PLT update sequence number. +type IncrementPltUpdateSequenceNumberCallbackPtr = FFI.FunPtr IncrementPltUpdateSequenceNumberCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapIncrementPltUpdateSequenceNumberCallback :: + IncrementPltUpdateSequenceNumberCallbackFFI -> IO IncrementPltUpdateSequenceNumberCallbackPtr + +-- | Callback function getting account index by address. +type GetAccountIndexByAddress = + -- | Address of the account to find + Types.AccountAddress -> + -- | The index of the account. `Nothing` is returned if the account does not exist + IO (Maybe Types.AccountIndex) + +-- | Internal helper function for mapping the 'GetAccountIndexByAddress' into the more +-- low-level function pointer which can be passed in FFI. +wrapGetAccountIndexByAddress :: GetAccountIndexByAddress -> IO GetAccountIndexByAddressCallbackPtr +wrapGetAccountIndexByAddress func = + ffiWrapGetAccountIndexByAddressCallback callback + where + callback :: GetAccountIndexByAddressCallbackFFI + callback accountAddressPtr accountIndexOutPtr = do + accountAddress <- Types.AccountAddress <$> (FFI.peek $ FFI.castPtr accountAddressPtr) + + accountIndexMaybe <- func accountAddress + case accountIndexMaybe of + Just accountIndex -> + do + FFI.poke accountIndexOutPtr (fromIntegral accountIndex) + return 0 + Nothing -> + return 1 + +-- | Callback function for getting account index by address. +-- +-- This is passed as a function pointer in FFI to call, see also 'GetAccountIndexByAddress' +-- for the more type-safe variant. +-- +-- See the corresponding function pointer definition in the Rust code for documentation of safety. +type GetAccountIndexByAddressCallbackFFI = + -- | Pointer for reading the 32 byte address of the account + FFI.Ptr Word.Word8 -> + -- | Pointer to where to write account index. Will be written to if status code is `0`. + FFI.Ptr Word.Word64 -> + -- | Status code: `0` if the account was found, else `1`. + IO Word.Word8 + +-- | The callback function pointer type for getting account index by address. +type GetAccountIndexByAddressCallbackPtr = FFI.FunPtr GetAccountIndexByAddressCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapGetAccountIndexByAddressCallback :: + GetAccountIndexByAddressCallbackFFI -> IO GetAccountIndexByAddressCallbackPtr + +-- | Callback function getting account address by index. +type GetAccountAddressByIndex = + -- | Index of the account to find + Types.AccountIndex -> + -- | The address of the account. `Nothing` is returned if the account does not exist + IO (Maybe Types.AccountAddress) + +-- | Internal helper function for mapping the 'GetAccountAddressByIndex' into the more +-- low-level function pointer which can be passed in FFI. +wrapGetAccountAddressByIndex :: GetAccountAddressByIndex -> IO GetAccountAddressByIndexCallbackPtr +wrapGetAccountAddressByIndex func = + ffiWrapGetAccountAddressByIndexCallback callback + where + callback :: GetAccountAddressByIndexCallbackFFI + callback accountIndex accountAddressOutPtr = do + accountAddressMaybe <- func (fromIntegral accountIndex) + case accountAddressMaybe of + Just (Types.AccountAddress accountAddressBytes) -> + do + FFI.poke (FFI.castPtr accountAddressOutPtr) accountAddressBytes + return 0 + Nothing -> + return 1 + +-- | Callback function for getting account address by index. +-- +-- This is passed as a function pointer in FFI to call, see also 'GetAccountAddressByIndex' +-- for the more type-safe variant. +-- +-- See the corresponding function pointer definition in the Rust code for documentation of safety. +type GetAccountAddressByIndexCallbackFFI = + -- | The account index of the account. + Word.Word64 -> + -- | Pointer for writing the 32 byte address of the account. + FFI.Ptr Word.Word8 -> + -- | Status code: `0` if the account was found, else `1`. + IO Word.Word8 + +-- | The callback function pointer type for getting account address by index. +type GetAccountAddressByIndexCallbackPtr = FFI.FunPtr GetAccountAddressByIndexCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapGetAccountAddressByIndexCallback :: + GetAccountAddressByIndexCallbackFFI -> IO GetAccountAddressByIndexCallbackPtr + +-- | Callback function getting token account states by account. +type GetTokenAccountStates = + -- | Index of the account. The account must exist. + Types.AccountIndex -> + -- | The token account states for the account paired with the index for the token. + IO [(Tokens.TokenIndex, AccountTokens.TokenAccountState)] + +-- | Internal helper function for mapping the 'GetTokenAccountStates' into the more +-- low-level function pointer which can be passed in FFI. +wrapGetTokenAccountStates :: GetTokenAccountStates -> IO GetTokenAccountStatesCallbackPtr +wrapGetTokenAccountStates func = + ffiWrapGetTokenAccountStatesCallback callback + where + callback :: GetTokenAccountStatesCallbackFFI + callback accountIndex = do + tokenAccountStates <- func (fromIntegral accountIndex) + let putStates = CS.putListOf S.put tokenAccountStates + let bytes = S.runPut putStates + BS.unsafeUseAsCStringLen bytes $ \(sourcePtr, len) -> + ContractStateFFI.copyToRustVec (FFI.castPtr sourcePtr) (fromIntegral len) + +-- | Callback function for getting token account states for an account. +-- +-- This is passed as a function pointer in FFI to call, see also 'GetTokenAccountStates' +-- for the more type-safe variant. +type GetTokenAccountStatesCallbackFFI = + -- | The account index of the account. + Word.Word64 -> + -- | Pointer to a Rust `Vec` allocated with `copy_to_vec_ffi` and which contains the + -- list of token index and token account state pairs in binary serialization. + IO (FFI.Ptr ContractStateFFI.Vec) + +-- | The callback function pointer type for getting account address by index. +type GetTokenAccountStatesCallbackPtr = FFI.FunPtr GetTokenAccountStatesCallbackFFI + +-- | Function to wrap Haskell functions or closures into a function pointer which can be passed over +-- FFI. +foreign import ccall "wrapper" + ffiWrapGetTokenAccountStatesCallback :: + GetTokenAccountStatesCallbackFFI -> IO GetTokenAccountStatesCallbackPtr diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Memory.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Memory.hs new file mode 100644 index 0000000000..2928aa3072 --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Memory.hs @@ -0,0 +1,14 @@ +-- | Module that implements memory management specific bindings. +-- +-- todo remove or change module as part of https://linear.app/concordium/issue/PSR-61/address-potentially-unsafe-behaviour-cased-by-using-shrink-to-fit +module Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Memory ( + rs_free_array_len_2, +) where + +import qualified Data.Word as Word +import qualified Foreign as FFI + +-- | Utility function shared by all instantations. Free an array that was +-- allocated on the heap, of the given size. +foreign import ccall unsafe "free_array_len_2" + rs_free_array_len_2 :: FFI.Ptr Word.Word8 -> Word.Word64 -> IO () diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Queries.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Queries.hs new file mode 100644 index 0000000000..39aa322ac9 --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Queries.hs @@ -0,0 +1,716 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Bindings into the Rust PLT Scheduler library. The module contains bindings to query PLTs. +-- +-- Each foreign imported function must match the signature of functions found on the Rust side. +module Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Queries ( + SerializedLockId, + queryPLTList, + queryTokenInfo, + queryTokenAccountInfos, + queryTokenAuthorizations, + queryLockList, + queryLockInfo, +) where + +import Control.Monad +import Control.Monad.IO.Class (liftIO) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Short as BSS +import qualified Data.ByteString.Unsafe as BS +import qualified Data.Map.Strict as Map +import qualified Data.Serialize as S +import qualified Data.Word as Word +import qualified Foreign as FFI +import qualified Foreign.C.Types as FFI + +import qualified Concordium.Types as Types +import qualified Concordium.Types.Locks as Locks +import qualified Concordium.Types.Queries.Locks as LockQueries +import qualified Concordium.Types.Queries.Tokens as QueriesTypes +import qualified Concordium.Utils.Serialization as CS + +import qualified Concordium.GlobalState.BlockState as BS +import qualified Concordium.GlobalState.ContractStateFFIHelpers as FFI +import qualified Concordium.GlobalState.Persistent.BlobStore as BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState as PLTBlockState +import qualified Concordium.GlobalState.Types as BS +import Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.BlockStateCallbacks +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Memory as Memory +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Status as Status +import qualified Data.FixedByteString as FBS +import qualified Data.Text.Encoding as Text + +-- | Get the list of all tokens, for protocol version where the PLT state is managed in Rust. +queryPLTList :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | The list of token ids + m [Types.TokenId] +queryPLTList bs = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryPLTListInBlobStoreMonad pltState queryCallbacks + where + -- Query the list of PLTs in the blob store monad. The function is a wrapper around an FFI call + -- to the Rust PLT Scheduler library. + queryPLTListInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block state to query. + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + -- Callback need for queries. + BlockStateQueryCallbacks -> + -- The list of token ids + m' [Types.TokenId] + queryPLTListInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + ffiQueryPLTList + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returned via out pointers + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + let getTokenIdList = S.isolate (BS.length returnData) $ CS.getListOf S.get + let tokenIdList = + either + (\message -> error $ "Token id list from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getTokenIdList returnData + return tokenIdList + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryPLTList' resulted in panic with message: " ++ show message) + _ -> error ("Unexpected status code from calling 'ffiQueryPLTList': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_plt_list`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_plt_list" + ffiQueryPLTList :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Output location for array containing return data, which is a list of token ids. + -- If the return value is `0`, the data is a serialized list of token ids. + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0`: the query was successful. + IO Word.Word8 + +-- | Get token info for a given token, for protocol version where the PLT state is managed in Rust. +queryTokenInfo :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | Token to find token info for. + Types.TokenId -> + -- | The token info. + m (Maybe QueriesTypes.TokenInfo) +queryTokenInfo bs tokenId = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryTokenInfoInBlobStoreMonad pltState queryCallbacks + where + -- Get token info for the given token in the given block state. The function is a wrapper around an FFI call + -- to the Rust PLT Scheduler library. + queryTokenInfoInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block state to query. + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + -- Callback need for queries. + BlockStateQueryCallbacks -> + -- The token info. + m' (Maybe QueriesTypes.TokenInfo) + queryTokenInfoInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + BSS.useAsCStringLen (Types.tokenId tokenId) $ \(tokenIdPtr, tokenIdLen) -> + ffiQueryTokenInfo + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + (FFI.castPtr tokenIdPtr) + (fromIntegral tokenIdLen) + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returned via out pointers + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + let getTokenInfo = S.isolate (BS.length returnData) S.get + let tokenInfo = + either + (\message -> error $ "Token info from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getTokenInfo returnData + return $ Just tokenInfo + Just Status.FSCFailed -> do + return Nothing + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryTokenInfo' resulted in panic with message: " ++ show message) + Nothing -> error ("Unexpected status code from calling 'ffiQueryTokenInfo': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_token_info`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- - `1`: The token does not exist +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_token_info" + ffiQueryTokenInfo :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Pointer to token id UTF-8 bytes. + FFI.Ptr Word.Word8 -> + -- | Byte length of token id UTF-8. + FFI.CSize -> + -- | Output location for array containing return data, which is the token info. + -- If the return value is `0`, the data is the serialized token info. + -- If the return value is `1`, the data is empty (zero bytes). + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0`: the query was successful. + -- * `1`: token does not exist + IO Word.Word8 + +-- | Get token authorizations for a given token, for protocol version where the PLT state is managed in Rust. +queryTokenAuthorizations :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | Token to find token info for. + Types.TokenId -> + -- | The token info. + m (Maybe QueriesTypes.TokenAuthorizations) +queryTokenAuthorizations bs tokenId = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryTokenAuthorizationsInBlobStoreMonad pltState queryCallbacks + where + -- Get token Authorizations for the given token in the given block state. The function is a wrapper around an FFI call + -- to the Rust PLT Scheduler library. + queryTokenAuthorizationsInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block state to query. + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + -- Callback need for queries. + BlockStateQueryCallbacks -> + -- The token info. + m' (Maybe QueriesTypes.TokenAuthorizations) + queryTokenAuthorizationsInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + BSS.useAsCStringLen (Types.tokenId tokenId) $ \(tokenIdPtr, tokenIdLen) -> + ffiQueryTokenAuthorizations + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + (FFI.castPtr tokenIdPtr) + (fromIntegral tokenIdLen) + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returned via out pointers + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + let getTokenAuthorizations = S.isolate (BS.length returnData) S.get + let tokenAuthorizations = + either + (\message -> error $ "Token authorizations from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getTokenAuthorizations returnData + return $ Just tokenAuthorizations + Just Status.FSCFailed -> do + return Nothing + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryTokenAuthorizations' resulted in panic with message: " ++ show message) + Nothing -> error ("Unexpected status code from calling 'ffiQueryTokenAuthorizations': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_token_authorizations`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- - `1`: The token does not exist +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_token_authorizations" + ffiQueryTokenAuthorizations :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Pointer to token id UTF-8 bytes. + FFI.Ptr Word.Word8 -> + -- | Byte length of token id UTF-8. + FFI.CSize -> + -- | Output location for array containing return data, which is the token authorizations. + -- If the return value is `0`, the data is the serialized token authorizations. + -- If the return value is `1`, the data is empty (zero bytes). + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0`: the query was successful. + -- * `1`: token does not exist + IO Word.Word8 + +-- | Get token account infos for a given account, for protocol version where the PLT state is managed in Rust. +queryTokenAccountInfos :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | Index of the account to find token account infos for. + Types.AccountIndex -> + -- | The token account infos. + m [QueriesTypes.Token] +queryTokenAccountInfos bs accountIndex = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryTokenAccountInfosInBlobStoreMonad pltState queryCallbacks + where + -- Get token account infos for the given account in the given block state. The function is a wrapper around an FFI call + -- to the Rust PLT Scheduler library. + queryTokenAccountInfosInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + -- Block state to query. + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + -- Callback need for queries. + BlockStateQueryCallbacks -> + -- The token account infos. + m' [QueriesTypes.Token] + queryTokenAccountInfosInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + -- Invoke the ffi call + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + ffiQueryTokenAccountInfos + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + (fromIntegral accountIndex) + returnDataPtrOutPtr + returnDataLenOutPtr + -- Free the function pointers we have just created + -- (loadCallbackPtr is created in another context, + -- so we should not free it) + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + -- Process the returned status and values returned via out pointers + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + let getTokenAccountInfos = S.isolate (BS.length returnData) $ CS.getListOf S.get + let tokenAccountInfos = + either + (\message -> error $ "Token account infos from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getTokenAccountInfos returnData + return tokenAccountInfos + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryTokenAccountInfos' resulted in panic with message: " ++ show message) + _ -> error ("Unexpected status code from calling 'ffiQueryTokenAccountInfos': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_token_account_infos`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_token_account_infos" + ffiQueryTokenAccountInfos :: + -- | Called to read data from blob store. + FFI.LoadCallback -> + -- | Called to get the token account balance in the haskell-managed block state. + ReadTokenAccountBalanceCallbackPtr -> + -- | Called to get account index by account address. + GetAccountIndexByAddressCallbackPtr -> + -- | Called to get account address by account index. + GetAccountAddressByIndexCallbackPtr -> + -- | Called to get token account states for account. + GetTokenAccountStatesCallbackPtr -> + -- | Pointer to the input PLT block state. + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | The account index to get token account infos for. + Word.Word64 -> + -- | Output location for array containing return data, which is a list of token account infos. + -- If the return value is `0`, the data is the serialized list of token account infos. + FFI.Ptr (FFI.Ptr Word.Word8) -> + -- | Output location for writing the length of the return data. + FFI.Ptr FFI.CSize -> + -- | Status code: + -- * `0`: the query was successful. + IO Word.Word8 + +-- | "Unlifts" the callback queries from the 'BlockStateQuery' monad into the IO monad, such that they can +-- be converted to FFI function pointers. +unliftBlockStateQueryCallbacks :: + forall m. + (Types.PVSupportsPLT (Types.MPV m), BS.BlockStateQuery m) => + BS.BlockState m -> + m BlockStateQueryCallbacks +unliftBlockStateQueryCallbacks bs = BS.withUnliftBSQ $ \unlift -> + do + let readTokenAccountBalance accountIndex tokenIndex = do + maybeAccount <- unlift $ BS.getAccountByIndex bs accountIndex + let account = snd $ maybe (error $ "Account with index does not exist: " ++ show accountIndex) id maybeAccount + unlift $ BS.getAccountTokenBalance account tokenIndex + getAccountIndexByAddress accountAddress = do + maybeAccount <- unlift $ BS.getAccount bs accountAddress + return $ fst <$> maybeAccount + getAccountAddressByIndex accountIndex = do + maybeAccount <- unlift $ BS.getAccountByIndex bs accountIndex + forM maybeAccount $ \(_, account) -> unlift $ BS.getAccountCanonicalAddress account + getTokenAccountStates accountIndex = do + maybeAccount <- unlift $ BS.getAccountByIndex bs accountIndex + let account = snd $ maybe (error $ "Account with index does not exist: " ++ show accountIndex) id maybeAccount + fmap Map.toList $ unlift $ BS.getAccountTokens account + + return BlockStateQueryCallbacks{..} + +-- | Get the list of all PLT lock ids, for a protocol version where the PLT state is +-- managed in Rust. Pattern follows 'queryPLTList'. +queryLockList :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | The list of lock ids + m [Locks.LockId] +queryLockList bs = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryLockListInBlobStoreMonad pltState queryCallbacks + where + queryLockListInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + BlockStateQueryCallbacks -> + m' [Locks.LockId] + queryLockListInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + ffiQueryLockList + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + returnDataPtrOutPtr + returnDataLenOutPtr + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> do + let getLockIdList = S.isolate (BS.length returnData) $ CS.getListOf S.get + let lockIdList = + either + (\message -> error $ "Lock id list from Rust PLT Scheduler could not be deserialized: " ++ message) + id + $ S.runGet getLockIdList returnData + return lockIdList + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryLockList' resulted in panic with message: " ++ show message) + _ -> error ("Unexpected status code from calling 'ffiQueryLockList': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_lock_list`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_lock_list" + ffiQueryLockList :: + FFI.LoadCallback -> + ReadTokenAccountBalanceCallbackPtr -> + GetAccountIndexByAddressCallbackPtr -> + GetAccountAddressByIndexCallbackPtr -> + GetTokenAccountStatesCallbackPtr -> + FFI.Ptr PLTBlockState.RustPLTBlockState -> + FFI.Ptr (FFI.Ptr Word.Word8) -> + FFI.Ptr FFI.CSize -> + IO Word.Word8 + +-- | Placeholder type representing the size of a serialized 'LockId' (24 bytes). +data LockIdSize + +instance FBS.FixedLength LockIdSize where + fixedLength _ = 24 + +-- | A serialized Lock ID as passed across the FFI boundary. +newtype SerializedLockId = SerializedLockId (FBS.FixedByteString LockIdSize) + deriving (FFI.Storable) + +-- | Get the CBOR-encoded `lock-info` payload for a given Lock ID, for a protocol version where the +-- PLT state is managed in Rust. The returned 'LockInfo' wraps the raw CBOR bytes verbatim; this +-- module never parses or re-encodes them. Pattern follows 'queryTokenInfo'. +queryLockInfo :: + forall m. + (Types.PVSupportsRustManagedPLT (Types.MPV m), BS.BlockStateQuery m) => + -- | Block state to query. + BS.BlockState m -> + -- | The serialized lock id to query. + SerializedLockId -> + -- | The lock info, or 'Nothing' if the lock does not exist. + m (Maybe LockQueries.LockInfo) +queryLockInfo bs lockId = do + queryCallbacks <- unliftBlockStateQueryCallbacks bs + pltState <- BS.getRustPLTBlockState bs + BS.liftBlobStore $ queryLockInfoInBlobStoreMonad pltState queryCallbacks + where + queryLockInfoInBlobStoreMonad :: + (BlobStore.MonadBlobStore m') => + PLTBlockState.ForeignPLTBlockStatePtr (Types.MPV m) -> + BlockStateQueryCallbacks -> + m' (Maybe LockQueries.LockInfo) + queryLockInfoInBlobStoreMonad + pltBlockState + queryCallbacks = + do + loadCallbackPtr <- fst <$> BlobStore.getCallbacks + liftIO $ FFI.alloca $ \returnDataPtrOutPtr -> FFI.alloca $ \returnDataLenOutPtr -> + do + readTokenAccountBalanceCallbackPtr <- wrapReadTokenAccountBalance $ readTokenAccountBalance queryCallbacks + getAccountIndexByAddressCallbackPtr <- wrapGetAccountIndexByAddress $ getAccountIndexByAddress queryCallbacks + getAccountAddressByIndexCallbackPtr <- wrapGetAccountAddressByIndex $ getAccountAddressByIndex queryCallbacks + getTokenAccountStatesCallbackPtr <- wrapGetTokenAccountStates $ getTokenAccountStates queryCallbacks + statusCode <- PLTBlockState.withPLTBlockState pltBlockState $ \pltBlockStatePtr -> + let SerializedLockId lockIdBytes = lockId + in FBS.withPtrReadOnly lockIdBytes $ \lockIdPtr -> + ffiQueryLockInfo + loadCallbackPtr + readTokenAccountBalanceCallbackPtr + getAccountIndexByAddressCallbackPtr + getAccountAddressByIndexCallbackPtr + getTokenAccountStatesCallbackPtr + pltBlockStatePtr + lockIdPtr + returnDataPtrOutPtr + returnDataLenOutPtr + FFI.freeHaskellFunPtr readTokenAccountBalanceCallbackPtr + FFI.freeHaskellFunPtr getAccountIndexByAddressCallbackPtr + FFI.freeHaskellFunPtr getAccountAddressByIndexCallbackPtr + FFI.freeHaskellFunPtr getTokenAccountStatesCallbackPtr + returnDataLen <- FFI.peek returnDataLenOutPtr + returnDataPtr <- FFI.peek returnDataPtrOutPtr + returnData <- + BS.unsafePackCStringFinalizer + returnDataPtr + (fromIntegral returnDataLen) + (Memory.rs_free_array_len_2 returnDataPtr (fromIntegral returnDataLen)) + case Status.parseStatusCode statusCode of + Just Status.FSCSuccess -> + return $ Just $ LockQueries.LockInfo returnData + Just Status.FSCFailed -> + return Nothing + Just Status.FSCPanic -> do + let message = case Text.decodeUtf8' returnData of + Right decoded -> decoded + Left _ -> "" + error ("Call to 'ffiQueryLockInfo' resulted in panic with message: " ++ show message) + Nothing -> error ("Unexpected status code from calling 'ffiQueryLockInfo': " ++ show statusCode) + +-- | C-binding for calling the Rust function `plt_scheduler::queries::query_lock_info`. +-- +-- Returns a byte representing the result: +-- +-- - `0`: The query was successful +-- - `1`: The lock does not exist +-- +-- See the exported function in the Rust code for documentation of safety. +foreign import ccall "ffi_query_lock_info" + ffiQueryLockInfo :: + FFI.LoadCallback -> + ReadTokenAccountBalanceCallbackPtr -> + GetAccountIndexByAddressCallbackPtr -> + GetAccountAddressByIndexCallbackPtr -> + GetTokenAccountStatesCallbackPtr -> + FFI.Ptr PLTBlockState.RustPLTBlockState -> + -- | Pointer to 24 bytes containing the serialized 'LockId'. + FFI.Ptr Word.Word8 -> + FFI.Ptr (FFI.Ptr Word.Word8) -> + FFI.Ptr FFI.CSize -> + IO Word.Word8 diff --git a/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Status.hs b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Status.hs new file mode 100644 index 0000000000..5069a946e4 --- /dev/null +++ b/concordium-consensus/src/Concordium/Scheduler/ProtocolLevelTokens/RustPLTScheduler/Status.hs @@ -0,0 +1,22 @@ +module Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler.Status (FFIStatusCode (..), parseStatusCode) where + +import qualified Data.Word as Word + +-- | Status code returned from the Rust scheduler library. +-- +-- This must match the @FfiStatusCode@ type defined on the rust side. +data FFIStatusCode + = -- | The call succeeded. + FSCSuccess + | -- | The call failed gracefully. + FSCFailed + | -- | The call resulted in a panic. + FSCPanic + +-- | Parse the word8 encoding of the @FfiStatusCode@ type found in the Rust library. +parseStatusCode :: Word.Word8 -> Maybe FFIStatusCode +parseStatusCode code = case code of + 0 -> Just FSCSuccess + 1 -> Just FSCFailed + 2 -> Just FSCPanic + _ -> Nothing diff --git a/concordium-consensus/src/Concordium/Scheduler/Runner.hs b/concordium-consensus/src/Concordium/Scheduler/Runner.hs index ae2894d8bf..501e5118da 100644 --- a/concordium-consensus/src/Concordium/Scheduler/Runner.hs +++ b/concordium-consensus/src/Concordium/Scheduler/Runner.hs @@ -138,6 +138,8 @@ transactionHelper t = return $ signTx keys meta (Types.encodePayload Types.ConfigureDelegation{..}) (TJSON meta TokenUpdate{..} keys) -> return $ signTx keys meta (Types.encodePayload Types.TokenUpdate{..}) + (TJSON meta MetaUpdate{..} keys) -> + return $ signTx keys meta (Types.encodePayload Types.MetaUpdate{..}) -- | Process account transactions. processTransactions :: (MonadFail m, MonadIO m) => [TransactionJSON] -> m [Types.AccountTransaction] @@ -302,7 +304,11 @@ data PayloadJSON { -- | Identifier of the token type to which the transaction refers. tuTokenId :: !Types.TokenId, -- | The CBOR-encoded operations to perform. - tuOperations :: !Types.TokenParameter + tuOperations :: !Types.RawCbor + } + | MetaUpdate + { -- | The CBOR-encoded operations to perform. + muOperations :: !Types.RawCbor } deriving (Show, Generic) diff --git a/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs b/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs index a816168dc7..56222fcac7 100644 --- a/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs +++ b/concordium-consensus/src/Concordium/Scheduler/TreeStateEnvironment.hs @@ -1,6 +1,7 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingVia #-} +{-# LANGUAGE EmptyCase #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} @@ -36,7 +37,7 @@ import Concordium.GlobalState.TreeState import Concordium.Kontrol.Bakers import Concordium.Logger import qualified Concordium.Scheduler as Sch -import qualified Concordium.Scheduler.EnvironmentImplementation as EnvImpl +import qualified Concordium.Scheduler.Environment as Env import Concordium.Scheduler.Types import Concordium.TimeMonad import qualified Concordium.TransactionVerification as TVer @@ -1176,14 +1177,9 @@ putBakerCommissionsInRange :: UpdatableBlockState m -> BakerId -> m (UpdatableBlockState m) -putBakerCommissionsInRange ranges bs (BakerId ai) = case protocolVersion @(MPV m) of - SP4 -> bsoConstrainBakerCommission bs ai ranges - SP5 -> bsoConstrainBakerCommission bs ai ranges - SP6 -> bsoConstrainBakerCommission bs ai ranges - SP7 -> bsoConstrainBakerCommission bs ai ranges - SP8 -> bsoConstrainBakerCommission bs ai ranges - SP9 -> bsoConstrainBakerCommission bs ai ranges - SP10 -> bsoConstrainBakerCommission bs ai ranges +putBakerCommissionsInRange ranges bs (BakerId ai) = case delegationSupport @(AccountVersionFor (MPV m)) of + SAVDelegationSupported -> bsoConstrainBakerCommission bs ai ranges + SAVDelegationNotSupported -> case protocolVersion @(MPV m) of {} -- | The result of executing the block prologue. data PrologueResult m = PrologueResult @@ -1285,14 +1281,14 @@ executeFrom blockHash slotNumber slotTime blockParent blockBaker mfinInfo newSee PrologueResult{..} <- executeBlockPrologue slotTime newSeedState oldChainParameters bshandle0 maxBlockEnergy <- gdMaxBlockEnergy <$> getGenesisData let context = - EnvImpl.ContextState + Env.ContextState { _chainMetadata = cm, _maxBlockEnergy = maxBlockEnergy, _accountCreationLimit = accountCreationLim } - (res, finState) <- EnvImpl.runSchedulerT (Sch.runTransactions txs) context (EnvImpl.makeInitialSchedulerState prologueBlockState) - let usedEnergy = finState ^. EnvImpl.ssEnergyUsed - let bshandle2 = finState ^. EnvImpl.ssBlockState + (res, finState) <- Env.runSchedulerT (Sch.runTransactions txs) context (Env.makeInitialSchedulerState prologueBlockState) + let usedEnergy = finState ^. Env.ssEnergyUsed + let bshandle2 = finState ^. Env.ssBlockState case res of Left fk -> Left fk <$ dropUpdatableBlockState bshandle2 Right outcomes -> do @@ -1311,7 +1307,7 @@ executeFrom blockHash slotNumber slotTime blockParent blockBaker mfinInfo newSee (newSeedState ^. epoch) prologueMintRewardParams mfinInfo - (finState ^. EnvImpl.ssExecutionCosts) + (finState ^. Env.ssExecutionCosts) counts prologueUpdates finalbsHandle <- freezeBlockState bshandle4 @@ -1365,18 +1361,18 @@ constructBlock slotNumber slotTime blockParent blockBaker mfinInfo newSeedState genData <- getGenesisData let maxBlockEnergy = gdMaxBlockEnergy genData let context = - EnvImpl.ContextState + Env.ContextState { _chainMetadata = cm, _maxBlockEnergy = maxBlockEnergy, _accountCreationLimit = accountCreationLim } (ft@Sch.FilteredTransactions{..}, finState) <- - EnvImpl.runSchedulerT (Sch.filterTransactions (fromIntegral maxSize) timeout transactionGroups) context (EnvImpl.makeInitialSchedulerState prologueBlockState) + Env.runSchedulerT (Sch.filterTransactions (fromIntegral maxSize) timeout transactionGroups) context (Env.makeInitialSchedulerState prologueBlockState) -- FIXME: At some point we should log things here using the same logging infrastructure as in consensus. - let usedEnergy = finState ^. EnvImpl.ssEnergyUsed - let bshandle2 = finState ^. EnvImpl.ssBlockState + let usedEnergy = finState ^. Env.ssEnergyUsed + let bshandle2 = finState ^. Env.ssBlockState bshandle3 <- bsoSetTransactionOutcomes bshandle2 (map snd ftAdded) let counts = countFreeTransactions (map (fst . fst) ftAdded) (isJust mfinInfo) @@ -1389,7 +1385,7 @@ constructBlock slotNumber slotTime blockParent blockBaker mfinInfo newSeedState (newSeedState ^. epoch) prologueMintRewardParams mfinInfo - (finState ^. EnvImpl.ssExecutionCosts) + (finState ^. Env.ssExecutionCosts) counts prologueUpdates diff --git a/concordium-consensus/src/Concordium/Startup.hs b/concordium-consensus/src/Concordium/Startup.hs index e914e809ba..25cdac43a0 100644 --- a/concordium-consensus/src/Concordium/Startup.hs +++ b/concordium-consensus/src/Concordium/Startup.hs @@ -33,6 +33,7 @@ import qualified Concordium.Genesis.Data as GenesisData import qualified Concordium.Genesis.Data.BaseV1 as GDBaseV1 import qualified Concordium.Genesis.Data.P1 as P1 import qualified Concordium.Genesis.Data.P10 as P10 +import qualified Concordium.Genesis.Data.P11 as P11 import qualified Concordium.Genesis.Data.P2 as P2 import qualified Concordium.Genesis.Data.P3 as P3 import qualified Concordium.Genesis.Data.P4 as P4 @@ -285,3 +286,9 @@ makeGenesisDataV1 { genesisCore = GDBaseV1.CoreGenesisParametersV1{..}, genesisInitialState = GenesisData.GenesisState{genesisAccounts = Vec.fromList genesisAccounts, ..} } + SP11 -> + GDP11 + P11.GDP11Initial + { genesisCore = GDBaseV1.CoreGenesisParametersV1{..}, + genesisInitialState = GenesisData.GenesisState{genesisAccounts = Vec.fromList genesisAccounts, ..} + } 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-consensus/test-runners/app/Main.hs b/concordium-consensus/test-runners/app/Main.hs index 7ed64d9730..46bd51d9e3 100644 --- a/concordium-consensus/test-runners/app/Main.hs +++ b/concordium-consensus/test-runners/app/Main.hs @@ -343,7 +343,8 @@ main = do { _ppBakerStakeThreshold = 300000000000 }, _cpFinalizationCommitteeParameters = NoParam, - _cpValidatorScoreParameters = NoParam + _cpValidatorScoreParameters = NoParam, + _cpMaxLockDuration = NoParam } let (genesisData, bakerIdentities, _) = makeGenesisDataV0 @PV diff --git a/concordium-consensus/test-runners/catchup/Main.hs b/concordium-consensus/test-runners/catchup/Main.hs index ed708cefb0..3cd1ab5972 100644 --- a/concordium-consensus/test-runners/catchup/Main.hs +++ b/concordium-consensus/test-runners/catchup/Main.hs @@ -372,7 +372,8 @@ main = do { _ppBakerStakeThreshold = 300000000000 }, _cpFinalizationCommitteeParameters = NoParam, - _cpValidatorScoreParameters = NoParam + _cpValidatorScoreParameters = NoParam, + _cpMaxLockDuration = NoParam } let (genesisData, bakerIdentities, _) = makeGenesisDataV0 @PV diff --git a/concordium-consensus/test-runners/deterministic/Main.hs b/concordium-consensus/test-runners/deterministic/Main.hs index 93ea2f92be..8a4623aebe 100644 --- a/concordium-consensus/test-runners/deterministic/Main.hs +++ b/concordium-consensus/test-runners/deterministic/Main.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -Wno-deprecations #-} @@ -290,7 +291,7 @@ initialState numAccts = do return SimState{..} where chainParams = - Dummy.dummyChainParameters + (Dummy.dummyChainParameters' @ChainParametersV1) { _cpConsensusParameters = ConsensusParametersV0{_cpElectionDifficulty = makeElectionDifficulty 50000}, _cpExchangeRates = makeExchangeRates 1 1, _cpFoundationAccount = maxBakerId + 1 diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs index f59ff093f8..68a3f054a6 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/CredentialDeploymentTests.hs @@ -101,7 +101,12 @@ testBB1 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "dca0b796dbfac96e7043942548c9d7cd470226740e2bdc793107de026d423e8d" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "dca0b796dbfac96e7043942548c9d7cd470226740e2bdc793107de026d423e8d" + SP8 -> read "9edf091441b19468d82a637c270472b0474592f3089e56415e4982351b095d35" + SP9 -> read "ad588c91476f5865dabf2ffe9b6954c924479aa0a2ef4057a5b79dbc110b1219" + SP10 -> read "5272b398aa5ade5ef14c6be41d586537f5212d96ea609b414bf2051e0c3780f7" + SP11 -> read "ece98c14e32372a737ad945cc67826dc0aa86849dc9fc5485059a406349ed1ec" } } where @@ -130,7 +135,12 @@ testBB2 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "3812ac3fa24c5676ea40c5879d9e88cd60e8af79d6ad7847c59df0880baacd01" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "3812ac3fa24c5676ea40c5879d9e88cd60e8af79d6ad7847c59df0880baacd01" + SP8 -> read "7ead7edff60ac23771f15052278304e3e2c9186826439ec7d04e28e55676f41b" + SP9 -> read "29d974384b047eaa8cb5b809dd37e9fe617e046ad22b7f9dbe605cdac9cf8e40" + SP10 -> read "29d974384b047eaa8cb5b809dd37e9fe617e046ad22b7f9dbe605cdac9cf8e40" + SP11 -> read "2bb51af7c0be873360ce949b0412acf83ced13924aabdaabf9e5f0d4126adbdb" } } where @@ -159,7 +169,12 @@ testBB3 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "cad9520a6cfac6e3f08a75394d68dcfbb9fa1a857d79e0048be5c7752ca72907" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "cad9520a6cfac6e3f08a75394d68dcfbb9fa1a857d79e0048be5c7752ca72907" + SP8 -> read "c8b3fc868c79703945638c709c9e2d03b67c3f70b023aac8ae5b980b41181726" + SP9 -> read "0282c255df3cb95180050ae3ee8838c0ab303fa7fe4e5e754ecf5d4c8db5152a" + SP10 -> read "0282c255df3cb95180050ae3ee8838c0ab303fa7fe4e5e754ecf5d4c8db5152a" + SP11 -> read "add91d5bf428e6b290770dc10a5f98027baa81cbf2291bb9d4bdce389d7eb922" } } where @@ -212,7 +227,12 @@ testBB2' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "c97b0cc90e29c62eb0e094696ef38891799360c0a89ad69b418205ab3b15b17a" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "c97b0cc90e29c62eb0e094696ef38891799360c0a89ad69b418205ab3b15b17a" + SP8 -> read "3abd796108d6fdcdf8c4361973d7152973cad3695b58b0c92a4e5021c0f80e33" + SP9 -> read "e10fe99a06aec8675f442ef0232ff0af9106ad6f5845335513603bcb4f3ff707" + SP10 -> read "0182d9155fb80b22fa43b8f7e1d9da389209bfa27340ec5a823a3f587e4455f9" + SP11 -> read "60b9953eeeb4e275fc4f452c546286e7e1b1b974128ce8207b38652588bbd9b1" } } where @@ -241,7 +261,12 @@ testBB3' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "f14475f1014ff13d2acf98f56a8f01823c1ae52ae01d53a8004f0e728c538357" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "f14475f1014ff13d2acf98f56a8f01823c1ae52ae01d53a8004f0e728c538357" + SP8 -> read "9e46988a9afd8470e25c33f2133d2c10cbb38050979957f200b4aca072e3c932" + SP9 -> read "075e1732e475a84b89f1ba89c06d58df7d7e42936dc16e0cf3d8b02dacf27c52" + SP10 -> read "2a9b336c419c9e64dc7db6735a297119649c4b579ac4d32ac58e1ab09302d17c" + SP11 -> read "cf30c95b37bcccf25d55197bad6ba3f5a0085d676a04ef818e1cb16aa24be214" } } where @@ -268,7 +293,12 @@ testBB4 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "9f757ba3e2fb79512a3c199fbd8b6c0a45eaef0fd3a5cc6a3ca78cf8f7ae18a6" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "9f757ba3e2fb79512a3c199fbd8b6c0a45eaef0fd3a5cc6a3ca78cf8f7ae18a6" + SP8 -> read "5bdf447992d82321a921bca9eeb6211bf3a290029164976885cf6b2fd14d923c" + SP9 -> read "ff90510a80285645170b5ff4614af366de2d8898e2bee111cae61e05ad640ada" + SP10 -> read "ff90510a80285645170b5ff4614af366de2d8898e2bee111cae61e05ad640ada" + SP11 -> read "1d6c66feef3c319291a67d1bdbf997b018747c369b8213828fd69bfebe6fb984" } } where @@ -295,7 +325,12 @@ testBB5 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "aca6161199f608947b265a048fd6dc404d31424a2a2a86d42be42310f6fd22a0" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "aca6161199f608947b265a048fd6dc404d31424a2a2a86d42be42310f6fd22a0" + SP8 -> read "e704583a45aec569aca9039e977d9a3a7c2db8bdfd650532182600b6a19cbb70" + SP9 -> read "6ad235a2db0349340044197b9c23da565f6bf4dd1ad40b89eab5bae7bcab0997" + SP10 -> read "6ad235a2db0349340044197b9c23da565f6bf4dd1ad40b89eab5bae7bcab0997" + SP11 -> read "809f53395251d5ab5093c379c3b2ad5c827b4e4fc4cf65188683b4a6a898c68a" } } where diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs index 55b023f45e..d89a7bf1de 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/EndToEnd/TransactionTableIntegrationTest.hs @@ -70,7 +70,12 @@ testBB1 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "796f0c4a934152c4f5a233d5120dbf1dd13370f3499af37c88b4ebf0601983b6" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "796f0c4a934152c4f5a233d5120dbf1dd13370f3499af37c88b4ebf0601983b6" + SP8 -> read "6f02a3e339abab1af9d23bc74369b6e142ad3b09f4d7042d16e076e225e3753f" + SP9 -> read "27ad830abd9d1f456f2a1666365ffff1fb216911d8dab2fbae5d3ad701816733" + SP10 -> read "5634a54bfdfb954205b78129c93236fba97b3190ad3fa412b4e2ffbaad6e7324" + SP11 -> read "b17a4151a48f2887083ccc76d5bc7568f539628ca1bf12031fb13bfa90e07f3c" } } where @@ -99,7 +104,12 @@ testBB2 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "c3585a5c1f76d7a8fa587b52c077fce936bd2c6f865afadd50068a61ea52d42e" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "c3585a5c1f76d7a8fa587b52c077fce936bd2c6f865afadd50068a61ea52d42e" + SP8 -> read "b0c8b7a3872b7bb35a9df1f620e47f2b7dad09889b29772d7bc03d713cff862d" + SP9 -> read "3017ca78e30e5bfc25a15849c07b1266c46185340b909e26aee439ddc694af7b" + SP10 -> read "3017ca78e30e5bfc25a15849c07b1266c46185340b909e26aee439ddc694af7b" + SP11 -> read "13964fd0b90be7dccd11fb38557e5ec2db096c3cf7a894c15d5119eac6ed4acb" } } where @@ -128,7 +138,12 @@ testBB3 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "a29f1403de7350e5791d985de61183c093c3e138ff81695908529ff876dbe49d" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "a29f1403de7350e5791d985de61183c093c3e138ff81695908529ff876dbe49d" + SP8 -> read "8adf29ed11f4784b4b32dbf84887ff9f5dd38ef2f78dbefe579822b48acd9e51" + SP9 -> read "8935d7fb4c2906ecf1e10b225e319fb39933ca7b0970b93084e4b7b6b01a8dca" + SP10 -> read "8935d7fb4c2906ecf1e10b225e319fb39933ca7b0970b93084e4b7b6b01a8dca" + SP11 -> read "152c02676295c287c1aa51bd264d54e70b49df37adf5fc0ed3df738e822a890d" } } where @@ -156,7 +171,12 @@ testBB4 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "9c2f41bb1e9ef0636bf67fe35755e2cc56be0c8e9a6e8b41defbdc7d1cdb945b" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "9c2f41bb1e9ef0636bf67fe35755e2cc56be0c8e9a6e8b41defbdc7d1cdb945b" + SP8 -> read "487c4bdf8af054727f0688bd8a7176f6c2a9c85dffc5d8acc1de23e0c3b6ef49" + SP9 -> read "fe5c4a3c4943440c34a72884ccf178af8f7b2043f0179b6bbf554574bb4ba581" + SP10 -> read "7ccdea365c8283b97af6b5d00fedc17853e84021dcbf1417c34fefbfe7736091" + SP11 -> read "31cd7c3a4a9fbfc4d7c32d8210506d7e7cbd1afbc925cb81d81b774a25da2d65" } } where diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/FinalizationRecover.hs b/concordium-consensus/tests/consensus/ConcordiumTests/FinalizationRecover.hs index 130ad1983f..40eb1b049c 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/FinalizationRecover.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/FinalizationRecover.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-deprecations #-} module ConcordiumTests.FinalizationRecover where @@ -55,7 +56,7 @@ genesis nBakers = [] 1234 Dummy.dummyKeyCollection - Dummy.dummyChainParameters + (Dummy.dummyChainParameters' @ChainParametersV0) makeFinalizationInstance :: BakerIdentity -> FinalizationInstance makeFinalizationInstance bid = FinalizationInstance (bakerSignKey bid) (bakerElectionKey bid) (bakerAggregationKey bid) diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs index ac3dd29a35..5ccbdf68f4 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/CatchUp.hs @@ -411,6 +411,25 @@ catchupWithTwoTimeoutsAtEndResponse _ = } assertCatchupResponse expectedTerminalData expectedBlocksServed =<< handleCatchUpRequest request =<< get +hashesRound4Block :: SProtocolVersion pv -> DerivableBlockHashes pv +hashesRound4Block sProtocolVersion = case sBlockHashVersionFor sProtocolVersion of + SBlockHashVersion0 -> + DerivableBlockHashesV0 + { dbhv0TransactionOutcomesHash = emptyBlockTOHV1 3, + dbhv0BlockStateHash = read "cdf730c1b3fdc6d07f404c6b95a4f3417c19653b1299b92f59fcaffcc9745910" + } + SBlockHashVersion1 -> + DerivableBlockHashesV1 + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "1a40cf446d0ad26c9cebf35e008c37685a3823e33b930b7d5dbbefffae411232" + SP8 -> read "c94306ddccb29ec5fe49846257de5a52a4ee132f74ed2152863416456f6b7df8" + spv + | TestBlocks.blockResultHashAsP9 spv -> + read "93837fbd7183afca6f32723f44f13f68a980b30e832ff93b5ed1c7a5e19ecdaa" + SP11 -> read "768ae2f059966bc0eef30974e51c172b33d61f9088366b7c853f84d08a5fc091" + spv -> TestBlocks.dummyBRH spv 0xc04 + } + catchupWithTwoBranchesResponse :: forall pv. (IsConsensusV1 pv, IsProtocolVersion pv) => @@ -443,16 +462,7 @@ catchupWithTwoBranchesResponse sProtocolVersion = bbEpochFinalizationEntry = Absent, bbNonce = computeBlockNonce (genesisLEN sProtocolVersion) 4 (TestBlocks.bakerVRFKey sProtocolVersion (3 :: Int)), bbTransactions = Vec.empty, - bbDerivableHashes = case sBlockHashVersionFor sProtocolVersion of - SBlockHashVersion0 -> - DerivableBlockHashesV0 - { dbhv0TransactionOutcomesHash = emptyBlockTOHV1 3, - dbhv0BlockStateHash = read "cdf730c1b3fdc6d07f404c6b95a4f3417c19653b1299b92f59fcaffcc9745910" - } - SBlockHashVersion1 -> - DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "1a40cf446d0ad26c9cebf35e008c37685a3823e33b930b7d5dbbefffae411232" - } + bbDerivableHashes = hashesRound4Block sProtocolVersion } TestBlocks.succeedReceiveBlock b4 -- There is one current timeout message and one current quorum message @@ -578,16 +588,7 @@ testMakeCatchupStatus sProtocolVersion = 4 (TestBlocks.bakerVRFKey sProtocolVersion (3 :: Int)), bbTransactions = Vec.empty, - bbDerivableHashes = case sBlockHashVersionFor sProtocolVersion of - SBlockHashVersion0 -> - DerivableBlockHashesV0 - { dbhv0TransactionOutcomesHash = emptyBlockTOHV1 3, - dbhv0BlockStateHash = read "cdf730c1b3fdc6d07f404c6b95a4f3417c19653b1299b92f59fcaffcc9745910" - } - SBlockHashVersion1 -> - DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "1a40cf446d0ad26c9cebf35e008c37685a3823e33b930b7d5dbbefffae411232" - } + bbDerivableHashes = hashesRound4Block sProtocolVersion } TestBlocks.succeedReceiveBlock b4 -- There is one current timeout message and one current quorum message diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs index 40b804252c..b024796711 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Common.hs @@ -149,10 +149,11 @@ forEveryProtocolVersion check = check SP4 "P4", check SP5 "P5", check SP6 "P6", - check SP7 "P7" - -- check SP8 "P8", - -- check SP9 "P9", - -- check SP10 "P10" + check SP7 "P7", + check SP8 "P8", + check SP9 "P9", + check SP10 "P10", + check SP11 "P11" ] -- | Run tests for each protocol version using consensus v1 (P6 and onwards). diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus.hs index f273c1059d..0b9001638a 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus.hs @@ -40,7 +40,7 @@ genesisDataV1 :: SProtocolVersion pv -> (GenesisData pv, [(BakerIdentity, FullBakerInfo)], Amount) genesisDataV1 sProtocolVersion = - makeGenesisDataV1 @pv + makeGenesisDataV1 0 10 3_600_000 @@ -50,7 +50,7 @@ genesisDataV1 sProtocolVersion = [ foundationAcct ] (withIsAuthorizationsVersionFor sProtocolVersion Dummy.dummyKeyCollection) - Dummy.dummyChainParameters + (Dummy.dummyChainParameters @pv) where foundationAcct = Dummy.createCustomAccount diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs index 341eb51157..16acacee80 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Consensus/Blocks.hs @@ -5,6 +5,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-overlapping-patterns #-} -- | This module tests block verification, processing, advancing round and baking. -- The below tests are intended to test the functionality exposed by the 'Concordium.KonsensusV1.Consensus.Blocks' module. @@ -19,8 +20,10 @@ import Data.Foldable import qualified Data.HashMap.Strict as HM import qualified Data.Map.Strict as Map import Data.Maybe +import qualified Data.Serialize as S import Data.Time import qualified Data.Vector as Vec +import Data.Word import Lens.Micro.Platform import Test.HUnit import Test.Hspec @@ -40,6 +43,7 @@ import Concordium.Types.Transactions import qualified Concordium.Types.Transactions as Transactions import qualified Concordium.Genesis.Data.P10 as P10 +import qualified Concordium.Genesis.Data.P11 as P11 import qualified Concordium.Genesis.Data.P6 as P6 import qualified Concordium.Genesis.Data.P7 as P7 import qualified Concordium.Genesis.Data.P8 as P8 @@ -81,7 +85,7 @@ genesisDataV1 :: SProtocolVersion pv -> (GenesisData pv, [(BakerIdentity, FullBakerInfo)], Amount) genesisDataV1 sProtocolVersion = - makeGenesisDataV1 @pv + makeGenesisDataV1 genTime (maxBaker + 1) genEpochDuration @@ -91,7 +95,7 @@ genesisDataV1 sProtocolVersion = [ foundationAcct ] (withIsAuthorizationsVersionFor sProtocolVersion Dummy.dummyKeyCollection) - Dummy.dummyChainParameters + (Dummy.dummyChainParameters @pv) where foundationAcct = Dummy.createCustomAccount @@ -133,6 +137,7 @@ genesisLEN sProtocolVersion = case sProtocolVersion of SP8 -> genesisLeadershipElectionNonce $ P8.genesisInitialState $ unGDP8 $ genesisData sProtocolVersion SP9 -> genesisLeadershipElectionNonce $ P9.genesisInitialState $ unGDP9 $ genesisData sProtocolVersion SP10 -> genesisLeadershipElectionNonce $ P10.genesisInitialState $ unGDP10 $ genesisData sProtocolVersion + SP11 -> genesisLeadershipElectionNonce $ P11.genesisInitialState $ unGDP11 $ genesisData sProtocolVersion -- | Full bakers at genesis genesisFullBakers :: forall pv. (IsConsensusV1 pv, IsProtocolVersion pv) => SProtocolVersion pv -> FullBakers @@ -304,6 +309,24 @@ setStateHash newStateHash block = case bbDerivableHashes block of hashes@DerivableBlockHashesV0{} -> block{bbDerivableHashes = hashes{dbhv0BlockStateHash = newStateHash}} +dummyBRH :: SProtocolVersion pv -> Word64 -> BlockResultHash +dummyBRH spv bid = case res of + Left e -> error e + Right r -> r + where + res = S.decode $ S.runPut $ do + S.putWord64be $ protocolVersionToWord64 (demoteProtocolVersion spv) + S.putWord64be bid + S.putWord64be 0 + S.putWord64be 0 + +-- | Helper that determines if the block result hash for this protocol version is computed as for +-- P9. +blockResultHashAsP9 :: SProtocolVersion pv -> Bool +blockResultHashAsP9 SP9 = True +blockResultHashAsP9 SP10 = True +blockResultHashAsP9 _ = False + -- | Valid block for round 1. testBB1 :: forall pv. (IsProtocolVersion pv, IsConsensusV1 pv) => BakedBlock pv testBB1 = @@ -325,7 +348,12 @@ testBB1 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "0970b0f7459e5150a56ac283eee6f587fc49cb1c3408146b46ee05457235bec7" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "0970b0f7459e5150a56ac283eee6f587fc49cb1c3408146b46ee05457235bec7" + SP8 -> read "69f807bdafa7ef495089545cf89837cbcd5d1424fe53770efae4608a8bbf7560" + spv | blockResultHashAsP9 spv -> read "747769e675019732bd4de37f2486ed696bea329b9c31b618a1e3a583ed5e4aaf" + SP11 -> read "ddc5b542193789c746a37220da7a1b4d5de4834298de11ea826ad9786c3deaf0" + spv -> dummyBRH spv 1 } } where @@ -353,7 +381,12 @@ testBB2 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "2e6636b8275663e44452650e4b7968ecb26a32d57998fbbccc0292fdecb1522d" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "2e6636b8275663e44452650e4b7968ecb26a32d57998fbbccc0292fdecb1522d" + SP8 -> read "8315e1c9ac06bf9b8d0902616dda6781648baef8765be529d583267ea6376a17" + spv | blockResultHashAsP9 spv -> read "6434d129fa41e0b469a056369f63e1f6eaafbfed3539c84c2f1542ae8e6cbcb2" + SP11 -> read "9351794e72cbc75c244d9f5b95b6771d8abf53f08f1d6b07c32fde2cc584bebe" + spv -> dummyBRH spv 2 } } where @@ -381,7 +414,12 @@ testBB3 = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "5777ce2df452ce52ee6beb43c555051588cdad67ad742e7be438cf9d22e31950" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "5777ce2df452ce52ee6beb43c555051588cdad67ad742e7be438cf9d22e31950" + SP8 -> read "aa38f2426aecf0103671767ececf2a5a5cbc6ebf20cb929d90f6cfd086bba258" + spv | blockResultHashAsP9 spv -> read "df8dd05c64cab1f1a6ba4c971aa751d1dfb653dfed3605a1334f601f1f808acf" + SP11 -> read "c05c990491621b293c8195899234964ce7f1401331478dfb8199bce6117cde14" + spv -> dummyBRH spv 3 } } where @@ -402,7 +440,12 @@ testBB2' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "04185ac844f6aea6e32b667debd1e9a337d67a80350d12f1cb813bf212a4bc23" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "04185ac844f6aea6e32b667debd1e9a337d67a80350d12f1cb813bf212a4bc23" + SP8 -> read "0b7a0106ac9293606424bf03e9f0314c3ac8e41a4097f30435eb9b2ee9463aa4" + spv | blockResultHashAsP9 spv -> read "bf7634ab27d1c7509d6d230f78e7df94eea7f53c04f3f92619c901b042a1b663" + SP11 -> read "40c1cbc93cdb41e0cf4b7ac67ab588cb01fd5be6b564b205cc34f4c0f0e8b6d5" + spv -> dummyBRH spv 0x102 } } where @@ -424,7 +467,12 @@ testBB3' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "11e59c1a721359a64b86a0c6bcee8f6cac7c5bc3a6b98517b0b4f8c5a726f9c5" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "11e59c1a721359a64b86a0c6bcee8f6cac7c5bc3a6b98517b0b4f8c5a726f9c5" + SP8 -> read "0a1d22fd9404974bb43616ad3d0152acb9de40f856b6fb66150b2c496730add5" + spv | blockResultHashAsP9 spv -> read "0d6386d76cff950e60543a03485cbbee5e11667e2ad09dc77a48cce5168dab58" + SP11 -> read "ea0ad5396417e2f087b67da69da65546246694bf3ed8720400943c57663bc195" + spv -> dummyBRH spv 0x103 } } where @@ -453,7 +501,12 @@ testBB4' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "ad8a288f88806037899d782b7dd3a37ade59e0d5f3e7a90b1db2b722ae9cbe3d" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "ad8a288f88806037899d782b7dd3a37ade59e0d5f3e7a90b1db2b722ae9cbe3d" + SP8 -> read "1afb0b0bd86301671549f1b1e407d71818d1d9d29c19beed584d17e2ace638d4" + spv | blockResultHashAsP9 spv -> read "874ad0c151a7e2ec3252af524de4348203f5ac607580dd99e1c70bce94787b93" + SP11 -> read "abad669bc34a4303f82adbe10dbf8dfa9bf689af57412f3dd6b1657085934a01" + spv -> dummyBRH spv 0x104 } } where @@ -474,7 +527,9 @@ testBB3'' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "daa799010a8b4acb47fa97b876abed73621db292029360734d9c8978b5859e7b" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "daa799010a8b4acb47fa97b876abed73621db292029360734d9c8978b5859e7b" + spv -> dummyBRH spv 0x203 } } where @@ -505,7 +560,12 @@ testBB1E = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "1811353ec3811af6241f3e5dc2e19740acf518f02dccc51f427310b8cfe9ca6c" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "1811353ec3811af6241f3e5dc2e19740acf518f02dccc51f427310b8cfe9ca6c" + SP8 -> read "a002fe8adf77ca8cb992fd25dd3c5430db7ee52e383212eeb0827170af323d2e" + spv | blockResultHashAsP9 spv -> read "f59f26a1b1da5858ccc51589c7eb9227493434aa0788f3fb8ffb3104ecebe4fb" + SP11 -> read "f24fb5200d24d7a59276602ddc465d06342e552eab3942e7b8760e8670412698" + spv -> dummyBRH spv 0xe01 } } where @@ -533,7 +593,12 @@ testBB2E = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "99fc52af1ee2336f0d353a84c4d6c15345882271f648f7b84e69d9c40d5571c2" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "99fc52af1ee2336f0d353a84c4d6c15345882271f648f7b84e69d9c40d5571c2" + SP8 -> read "af4910f96957a8fc36844cfd1ca9dfadd47776b2a5b4e5f23011b4f580a01902" + spv | blockResultHashAsP9 spv -> read "8e068776dc01e0079a00d9b531897e2a1e732c91ce9db92d64fd2cd183cea83e" + SP11 -> read "3709ae5e239db9df50edf3690119f6f3381d2c40e986074368b4b3b52a994203" + spv -> dummyBRH spv 0xe02 } } where @@ -563,7 +628,12 @@ testBB3EX = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "0236b72bafe1575fdde0b01b35d24ad16613569600b83ed46b7e17c3c3dbaf28" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "0236b72bafe1575fdde0b01b35d24ad16613569600b83ed46b7e17c3c3dbaf28" + SP8 -> read "854f08741a720a65b136eceae61cdb5fa59dd5f1af8bdcb58e41e850fec5034d" + spv | blockResultHashAsP9 spv -> read "abe0485900b7b651c670f2fa506534ec2bc095f6d586a92f5b5b1160ca73c58c" + SP11 -> read "19c23ccfb4a0d3ef538f1ffadb1968dda0a56bd47f1503612960feeaf505b6d7" + spv -> dummyBRH spv 0x1e03 } } where @@ -609,7 +679,12 @@ testBB3E = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "2a12a1b02d8cfb835d6f572ffd3a0156145ec2142b47ef7b9e9495b61ff241b7" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "2a12a1b02d8cfb835d6f572ffd3a0156145ec2142b47ef7b9e9495b61ff241b7" + SP8 -> read "d3cf0aa2e2c6fc2fd7d0be7f4192cc8d04e3afce1041d194a2c9ae437bc4aad6" + spv | blockResultHashAsP9 spv -> read "87df979129f4462d51ca7214068557add1e4ecbb7ef097f2dc76bcc9ea7c90bb" + SP11 -> read "1b2e3dc1b52845d3ecaaf92cea7e75b91a237ac7e512e8e1be89039f0918a3a6" + spv -> dummyBRH spv 0xe03 } } where @@ -648,7 +723,12 @@ testBB4E = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "66cf8d97a956e2306e60337848775d606f575bd48f4d1e4420d4cf579d5bfb0e" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "66cf8d97a956e2306e60337848775d606f575bd48f4d1e4420d4cf579d5bfb0e" + SP8 -> read "d11da8da6f4a14fe5221847690e754450e79d65ad7e7ab1606f4f14d333f3a53" + spv | blockResultHashAsP9 spv -> read "5f84d37c5c7d22893a285f3229d3b19dc9933e8220dbce84dc3f5bb7d1e325f1" + SP11 -> read "9cf6abe73d9c5f246da58e3d9be426089a5183334fd91f3b2e4d51804cd0b7c8" + spv -> dummyBRH spv 0xe04 } } where @@ -671,7 +751,12 @@ testBB4E' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "e0d460456228a923c4d0116b6add192b491279b24ce160067652e1afa11bac56" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "e0d460456228a923c4d0116b6add192b491279b24ce160067652e1afa11bac56" + SP8 -> read "51894272a8818fc7ad4290f921bb2ff0488a098ebd0697b479136ec6ae5f5ed7" + spv | blockResultHashAsP9 spv -> read "e44e1e7f0eac84030f80fe191c8ca9216da54a8533b9602c249d9b0a89b08ed9" + SP11 -> read "e4dbf5cb062808b8f28149717751bf2f87d949ea2e921de40fae2f8b950a888c" + spv -> dummyBRH spv 0x1e04 } } where @@ -701,7 +786,12 @@ testBB5E' = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "2c8ff8fbc07b5e1486ebad3e241fa0aefdb7637651c6120b0a50a27057f7431a" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "2c8ff8fbc07b5e1486ebad3e241fa0aefdb7637651c6120b0a50a27057f7431a" + SP8 -> read "970f5145277abea69844b53189c803ad7d5a8fd19105208ae4c580957224099a" + spv | blockResultHashAsP9 spv -> read "d15789473fdb0b472bd1e1c2590f2b069ffe4c58a7079fec0e4f737a46d2c3f5" + SP11 -> read "ace223d0f67529552b942e77339410a5609ed5c8dfb63a1c57b64fd0c786b82d" + spv -> dummyBRH spv 0x1e05 } } where @@ -744,7 +834,12 @@ testBB2Ex = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "a562963223c07cbcee46e78bba06968d578120b71eaf59d8ce12f3f384b21f47" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "a562963223c07cbcee46e78bba06968d578120b71eaf59d8ce12f3f384b21f47" + SP8 -> read "85974099bbc92a64a27df7d057d130dd4eabcc0a8e605f701b449db3e68b3c3b" + spv | blockResultHashAsP9 spv -> read "781624811b67e3c24f2e0d4b3596e4b973fe52306ba43448a47748e6f25269a0" + SP11 -> read "6ac249ded954708e952488db0d63da831e11f0017590f9651110ed5aafa6e673" + spv -> dummyBRH spv 0x0f02 } } where @@ -789,7 +884,12 @@ testBB3Ex = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "2ea4f556dc29b5a1774635cb670f7b9aa3182eb2cc685b0e4f59daf9541cb539" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "2ea4f556dc29b5a1774635cb670f7b9aa3182eb2cc685b0e4f59daf9541cb539" + SP8 -> read "fcb11273e588af86426c36f3d825d6eaf3d6db29339a071253ce764b4be3d45e" + spv | blockResultHashAsP9 spv -> read "0cf1a98c6f1c0798cc2441633cabada717c2c9c2344f9f8593934ae5abf1d56f" + SP11 -> read "34a1b4dbae466b3d0e378301c4a8634daf4263f286c88f56eae8150740614930" + spv -> dummyBRH spv 0x1e03 } } where @@ -822,7 +922,12 @@ testBB3EA = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "99fc52af1ee2336f0d353a84c4d6c15345882271f648f7b84e69d9c40d5571c2" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "99fc52af1ee2336f0d353a84c4d6c15345882271f648f7b84e69d9c40d5571c2" + SP8 -> read "af4910f96957a8fc36844cfd1ca9dfadd47776b2a5b4e5f23011b4f580a01902" + spv | blockResultHashAsP9 spv -> read "8e068776dc01e0079a00d9b531897e2a1e732c91ce9db92d64fd2cd183cea83e" + SP11 -> read "3709ae5e239db9df50edf3690119f6f3381d2c40e986074368b4b3b52a994203" + spv -> dummyBRH spv 0x2f03 } } where @@ -855,7 +960,12 @@ testBB4EA = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "e0d460456228a923c4d0116b6add192b491279b24ce160067652e1afa11bac56" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "e0d460456228a923c4d0116b6add192b491279b24ce160067652e1afa11bac56" + SP8 -> read "e5c82e4ff33f6f068704a4da8a829e3e27659df26512a5b6449d4e27c50b0c5d" + spv | blockResultHashAsP9 spv -> read "71df1f4743ed4401ff0ec3d3b8dc3edc66b639f3aa2c564f583a7c622fa46f76" + SP11 -> read "12ebd8153f03a666c170f50c820680c434552acd42b7a1d333a51bfe34119132" + spv -> dummyBRH spv 0x2f04 } } where @@ -902,7 +1012,13 @@ testBB1T = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "e9f625190459013f6530d044d80d91182b91f440cbb0a8933d5ba9e5dff06236" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "e9f625190459013f6530d044d80d91182b91f440cbb0a8933d5ba9e5dff06236" + SP8 -> read "928a6ab0ca2a38086812bdbf39d46737345c358479d35c435f97c609ee0e215d" + SP9 -> read "ba0d5ccac35703d012901d95669aea8088530b89d0e887f76c0b0ade4dd9c829" + SP10 -> read "4a3d62fc566532a0151727c50cd80190e07f44b474f7074623b1e89646be9044" + SP11 -> read "f19f857e16e53dcdffd85fcc84900e832bdde3054431df17fdb8131dde6dd1c1" + spv -> dummyBRH spv 0x7001 } } where @@ -930,7 +1046,13 @@ testBB2T = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "a8a6f224567aaac2ef40a7ddc2b186c47485acd3bb685d6216af25fdddd9b580" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "a8a6f224567aaac2ef40a7ddc2b186c47485acd3bb685d6216af25fdddd9b580" + SP8 -> read "60905a3baea93cf9564621ff1d8bbbc2469b50f26d101d125a092d104ca4fe16" + SP9 -> read "691a94879d0b135e6b55bb8afdc6740f4a29d0a5ab94670bec0fb02b36291bd0" + SP10 -> read "3a1fe79e0c5df5a73a55a02c9f410da73e10cfb02094fd053ef180ca3d6d8bac" + SP11 -> read "27f7a5f1dcc9f1efef2cda7744724852b460bc23645f8378fc2bcd4eb72ac4e1" + spv -> dummyBRH spv 0x7002 } } where @@ -964,7 +1086,13 @@ testBB3T = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "a502a11146284e4667d509da46a8c0c437533a1325d76d8e0b2422c00be90bca" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "a502a11146284e4667d509da46a8c0c437533a1325d76d8e0b2422c00be90bca" + SP8 -> read "8f1d79480a04412221a6a90c02ff98b8c59f08510067691c005f2e36b3c32563" + SP9 -> read "51fbaf0502c838c7455786ac3c0397c51f3e6116a71cdc4a759b3a4bd243907f" + SP10 -> read "eafb304cf3a771810688878fbc3b19d265d3728c455d9ae4941deacdbd7abbc6" + SP11 -> read "7195ed6bf75d9b12603d857a4dc31b61ca25934fe3879d29ff8e94ad970c863b" + spv -> dummyBRH spv 0x7003 } } where @@ -993,7 +1121,13 @@ testBB4T = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "9fd40b91df19dd8391585b8497c493f950e1b2615680a8693bfded6172bd896d" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "9fd40b91df19dd8391585b8497c493f950e1b2615680a8693bfded6172bd896d" + SP8 -> read "efbd04f230226d140dcb1bd9a4eb06c0583a2b6edfa16dac2dde895383559998" + SP9 -> read "0f73ad403626533632c43f2ca5938f9c3abb658df73a45328a5ca8c6630a973b" + SP10 -> read "0f73ad403626533632c43f2ca5938f9c3abb658df73a45328a5ca8c6630a973b" + SP11 -> read "27735405c5fe492c798448768963a7dd19b1fffb679610af7dc17c97abaa6e44" + spv -> dummyBRH spv 0x7004 } } where @@ -1022,7 +1156,13 @@ testBB5T = } SBlockHashVersion1 -> DerivableBlockHashesV1 - { dbhv1BlockResultHash = read "d9ebdef2097ed38d5eececb6cf30c611c2d2d3a54bf7231aec40fc0c674156b4" + { dbhv1BlockResultHash = case sProtocolVersion of + SP7 -> read "d9ebdef2097ed38d5eececb6cf30c611c2d2d3a54bf7231aec40fc0c674156b4" + SP8 -> read "242e8f6bf7e2a6fc9263b95b6e233b43010308d061e1f511b920ff7deea3ac9f" + SP9 -> read "f375cd289ab3c51ff09cb910dcc89d942ea26a3c22904b0dd37a933c21805634" + SP10 -> read "f375cd289ab3c51ff09cb910dcc89d942ea26a3c22904b0dd37a933c21805634" + SP11 -> read "4fdee647224fee55a572130c9a230ba274e5aefd37f1803dde49b897a6a41290" + spv -> dummyBRH spv 0x7005 } } where @@ -2164,7 +2304,7 @@ testReceiveWithTransactions sProtocolVersion = tests :: Spec tests = describe "KonsensusV1.Consensus.Blocks" $ do - describe "uponReceiveingBlockPV" $ do + describe "uponReceivingBlockPV" $ do Common.forEveryProtocolVersionConsensusV1 $ \spv pvString -> describe pvString $ do testReceive3 spv diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs index 12f5eaf63a..2cea0acfb6 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/Timeout.hs @@ -73,7 +73,7 @@ genesisDataV1 sProtocolVersion = [ foundationAcct ] (withIsAuthorizationsVersionFor sProtocolVersion Dummy.dummyKeyCollection) - Dummy.dummyChainParameters + (Dummy.dummyChainParameters @pv) where foundationAcct = Dummy.createCustomAccount @@ -667,7 +667,7 @@ testExecuteTimeoutMessages sProtocolVersion = [ foundationAcct ] (withIsAuthorizationsVersionFor sProtocolVersion Dummy.dummyKeyCollection) - Dummy.dummyChainParameters + (Dummy.dummyChainParameters @pv) -- | Tests the 'checkTimeoutCertificate' function. testCheckTimeoutCertificate :: diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs index 47518468e3..89edb26927 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/KonsensusV1/TransactionProcessingTest.hs @@ -53,6 +53,7 @@ import Concordium.Genesis.Data hiding (GenesisConfiguration) import qualified Concordium.Genesis.Data.Base as Base import Concordium.Genesis.Data.BaseV1 import Concordium.Genesis.Data.P10 +import Concordium.Genesis.Data.P11 import Concordium.Genesis.Data.P6 import Concordium.Genesis.Data.P7 import Concordium.Genesis.Data.P8 @@ -160,6 +161,7 @@ newtype NoLoggerT m a = NoLoggerT {runNoLoggerT :: m a} instance (Monad m) => MonadLogger (NoLoggerT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () -- | A test monad that is suitable for testing transaction processing -- as it derives the required capabilities. @@ -260,7 +262,7 @@ makeTestingGenesisData idps = withIsAuthorizationsVersionFor (protocolVersion @pv) (dummyKeyCollection @(AuthorizationsVersionFor pv)) - genesisChainParameters = dummyChainParameters @(ChainParametersVersionFor pv) + genesisChainParameters = dummyChainParameters @pv genesisLeadershipElectionNonce = Hash.hash "LeadershipElectionNonce" genesisAccounts = Vec.fromList $ makeFakeBakers 1 in case protocolVersion @pv of @@ -294,6 +296,12 @@ makeTestingGenesisData idps = { genesisCore = coreGenesisParams, genesisInitialState = Base.GenesisState{..} } + SP11 -> + GDP11 + GDP11Initial + { genesisCore = coreGenesisParams, + genesisInitialState = Base.GenesisState{..} + } -- | Utility function for parrsing identity providers. readIps :: BSL.ByteString -> Maybe IdentityProviders @@ -384,7 +392,7 @@ dummyUpdateInstruction effTime = { _cpltTokenId = TokenId "dummyToken", _cpltTokenModule = TokenModuleRef $ Hash.hash "dummyToken", _cpltDecimals = 4, - _cpltInitializationParameters = TokenParameter "" + _cpltInitializationParameters = RawCbor "" } -- | The block item for 'dummyNormalTransaction'. @@ -426,7 +434,7 @@ testTransactionVerification _ = describe "transaction verification" $ do -- Create a context suitable for verifying a transaction within a 'Individual' context. getCtx = do _ctxBs <- bpState <$> gets' _lastFinalized - let chainParams = dummyChainParameters @(ChainParametersVersionFor pv) + let chainParams = dummyChainParameters @pv let _ctxMaxBlockEnergy = chainParams ^. cpConsensusParameters . cpBlockEnergyLimit return $! Context{_ctxTransactionOrigin = TVer.Individual, ..} diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs b/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs index 02abdef410..b124ec5d82 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/MerkleProofs.hs @@ -59,7 +59,7 @@ propBBMerkleProofParse spv = theTest :: BakedBlock pv -> Property theTest bb@BakedBlock{..} = let proof = runIdentity (buildMerkleProof (const True) bb) - in case uncurry parseMerkleProof blockSchema proof of + in case uncurry parseMerkleProof (blockSchema $ demoteProtocolVersion spv) proof of Left err -> counterexample ("Failed to parse proof" ++ show err) False Right (pt, hsh) -> blockHash (getHash bb) === hsh diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/PassiveFinalization.hs b/concordium-consensus/tests/consensus/ConcordiumTests/PassiveFinalization.hs index 3dd1ffc7df..039a8089ac 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/PassiveFinalization.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/PassiveFinalization.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-orphans -Wno-deprecations #-} @@ -32,7 +33,7 @@ import qualified Concordium.GlobalState.AccountMap.LMDB as LMDBAccountMap import Concordium.GlobalState.BakerInfo import Concordium.GlobalState.Block import qualified Concordium.GlobalState.BlockPointer as BS -import Concordium.GlobalState.DummyData (dummyChainParameters, dummyKeyCollection) +import Concordium.GlobalState.DummyData (dummyChainParameters', dummyKeyCollection) import Concordium.GlobalState.Finalization import Concordium.GlobalState.Parameters import Concordium.GlobalState.Persistent.Account @@ -344,7 +345,7 @@ createInitStates additionalFinMembers = do _ -> error "bis should be a list with four elements" let bakerAccounts = map (\(_, _, acc, _) -> acc) bis - cps = dummyChainParameters & cpConsensusParameters . cpElectionDifficulty .~ makeElectionDifficultyUnchecked 100000 + cps = dummyChainParameters' @'ChainParametersV0 & cpConsensusParameters . cpElectionDifficulty .~ makeElectionDifficultyUnchecked 100000 gen = GDP1 GDP1Initial diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/ReceiveTransactionsTest.hs b/concordium-consensus/tests/consensus/ConcordiumTests/ReceiveTransactionsTest.hs index f469ddbc44..4c6b710d0c 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/ReceiveTransactionsTest.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/ReceiveTransactionsTest.hs @@ -3,6 +3,7 @@ {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} @@ -217,6 +218,7 @@ instance (MonadReader r m) => MonadReader r (FixedTimeT m) where instance (Monad m) => MonadLogger (FixedTimeT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () instance (Monad m) => TimeMonad (FixedTimeT m) where currentTime = FixedTime return @@ -238,6 +240,7 @@ newtype NoLoggerT m a = NoLoggerT {runNoLoggerT :: m a} instance (Monad m) => MonadLogger (NoLoggerT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () -- | Run the given computation in a state consisting of only the genesis block and the state determined by it. runTestSkovQueryMonad' :: TestSkovQueryMonad a -> UTCTime -> GenesisData PV -> IO (a, TestSkovState) @@ -280,7 +283,7 @@ maxBlockEnergy = 3_000_000 -- | Construct a genesis state with hardcoded values for parameters that should not affect this test. -- Modify as you see fit. testGenesisData :: UTCTime -> IdentityProviders -> AnonymityRevokers -> CryptographicParameters -> GenesisData PV -testGenesisData now ips ars cryptoParams = makeTestingGenesisDataP5 (utcTimeToTimestamp now) 1 1 1 dummyFinalizationCommitteeMaxSize cryptoParams ips ars maxBlockEnergy dummyKeyCollection dummyChainParameters +testGenesisData now ips ars cryptoParams = makeTestingGenesisDataP5 (utcTimeToTimestamp now) 1 1 1 dummyFinalizationCommitteeMaxSize cryptoParams ips ars maxBlockEnergy dummyKeyCollection (dummyChainParameters @PV) -- | Run the doReceiveTransaction function and obtain the results testDoReceiveTransaction :: [BlockItem] -> Slot -> TestSkovQueryMonad [(TransactionHash, UpdateResult)] diff --git a/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs b/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs index 7a0e25a2ea..48bef68494 100644 --- a/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs +++ b/concordium-consensus/tests/consensus/ConcordiumTests/Update.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-orphans -Wno-deprecations #-} @@ -107,7 +108,7 @@ createInitStates dir = do -- This does not happen due to how `bis` is constructed _ -> error "bis should be a list with two elements" bakerAccounts = (^. _3) <$> bis - cps = Dummy.dummyChainParameters & cpConsensusParameters . cpElectionDifficulty .~ makeElectionDifficultyUnchecked 100000 + cps = Dummy.dummyChainParameters' @ChainParametersV0 & cpConsensusParameters . cpElectionDifficulty .~ makeElectionDifficultyUnchecked 100000 gen = GDP1 GDP1Initial diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/AccountReleaseScheduleTest.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/AccountReleaseScheduleTest.hs index 1e923d982f..1d613deac5 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/AccountReleaseScheduleTest.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/AccountReleaseScheduleTest.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -Wno-deprecations #-} module GlobalStateTests.AccountReleaseScheduleTest (tests) where @@ -43,6 +44,7 @@ newtype NoLoggerT m a = NoLoggerT {runNoLoggerT :: m a} instance (Monad m) => MonadLogger (NoLoggerT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () type ThisMonadConcrete pv = PBS.PersistentBlockStateMonad @@ -84,7 +86,7 @@ createGS = do dummyIdentityProviders dummyArs dummyKeyCollection - dummyChainParameters + (dummyChainParameters @PV) -- save the block state so accounts are written to the lmdb database. void $ saveBlockState initState addr0 <- BS.accountCanonicalAddress acc0 diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs index 8583e4cc3c..850b7fcacb 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Accounts.hs @@ -53,6 +53,7 @@ newtype NoLoggerT m a = NoLoggerT {runNoLoggerT :: m a} instance (Monad m) => MonadLogger (NoLoggerT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () assertRight :: Either String a -> Assertion assertRight (Left e) = assertFailure e diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/BlockStateHelpers.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/BlockStateHelpers.hs index 6ae83349a7..e992b7bb11 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/BlockStateHelpers.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/BlockStateHelpers.hs @@ -298,7 +298,7 @@ checkActiveBakers bs = do DummyData.dummyIdentityProviders DummyData.dummyArs (withIsAuthorizationsVersionFor spv DummyData.dummyKeyCollection) - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @pv) dumpState :: (SupportsPersistentState pv m) => HashedPersistentBlockState pv -> m () dumpState hpbs = do diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureDelegator.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureDelegator.hs index 14e49a06b1..f789cc87d2 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureDelegator.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureDelegator.hs @@ -242,7 +242,7 @@ runAddDelegatorTest spv dtc@DelegatorTestConfig{..} da@DelegatorAdd{..} = runTes where flexibleCooldown = sSupportsFlexibleCooldown (sAccountVersionFor spv) chainParams = - DummyData.dummyChainParameters @(ChainParametersVersionFor pv) + DummyData.dummyChainParameters @pv & cpPoolParameters . ppCapitalBound .~ dtcCapitalBound & cpPoolParameters . ppLeverageBound .~ dtcLeverageBound mkInitialState accounts = @@ -435,7 +435,7 @@ runUpdateDelegatorTest spv dtc@DelegatorTestConfig{..} du@DelegatorUpdate{..} = where flexibleCooldown = sSupportsFlexibleCooldown (sAccountVersionFor spv) chainParams = - DummyData.dummyChainParameters @(ChainParametersVersionFor pv) + DummyData.dummyChainParameters @pv & cpPoolParameters . ppCapitalBound .~ dtcCapitalBound & cpPoolParameters . ppLeverageBound .~ dtcLeverageBound (oldCapital, oldRestake, oldTarget, oldPendingChange) = diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureValidator.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureValidator.hs index 2c31f51614..77667e2f7c 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureValidator.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/ConfigureValidator.hs @@ -113,7 +113,7 @@ testAddValidatorAllCases spv = describe "bsoAddValidator" $ do supportSuspension = supportsValidatorSuspension $ accountVersionFor $ demoteProtocolVersion (protocolVersion @pv) minEquity = 1_000_000_000 chainParams = - DummyData.dummyChainParameters @(ChainParametersVersionFor pv) + DummyData.dummyChainParameters @pv & cpPoolParameters . ppMinimumEquityCapital .~ minEquity & cpPoolParameters . ppCommissionBounds .~ CommissionRanges @@ -565,7 +565,7 @@ runUpdateValidatorTest spv commissionRanges ValidatorUpdateConfig{vucValidatorUp hasValidatorSuspension = sSupportsValidatorSuspension (accountVersion @(AccountVersionFor pv)) minEquity = 1_000_000_000 chainParams = - DummyData.dummyChainParameters @(ChainParametersVersionFor pv) + DummyData.dummyChainParameters @pv & cpPoolParameters . ppMinimumEquityCapital .~ minEquity & cpPoolParameters . ppCommissionBounds .~ commissionRanges diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/CooldownProcessing.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/CooldownProcessing.hs index 7c7c7f5074..266d3dc21d 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/CooldownProcessing.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/CooldownProcessing.hs @@ -40,7 +40,7 @@ propProcessPrePreCooldowns cds = runTestBlockState @P7 $ do DummyData.dummyIdentityProviders DummyData.dummyArs DummyData.dummyKeyCollection - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @P7) bs' <- bsoProcessPrePreCooldowns (hpbsPointers initialBS) newCooldowns <- checkCooldowns bs' liftIO $ assertEqual "Cooldowns" (processPrePreCooldown <$> cds) newCooldowns @@ -57,7 +57,7 @@ propProcessCooldowns cds expire new = runTestBlockState @P7 $ do DummyData.dummyIdentityProviders DummyData.dummyArs DummyData.dummyKeyCollection - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @P7) bs' <- bsoProcessCooldowns (hpbsPointers initialBS) expire new newCooldowns <- checkCooldowns bs' liftIO $ assertEqual "Cooldowns" (processPreCooldown new . processCooldowns expire <$> cds) newCooldowns diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs index b7469a98b7..1542ef0f3d 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Instances.hs @@ -477,6 +477,7 @@ instance (IsProtocolVersion pv) => MonadProtocolVersion (TestMonad pv) where -- Do not log anything. instance MonadLogger (TestMonad pv) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () instance MonadModuleMapStore (TestMonad pv) where insertModules mods = TestMonad $ \r _ _ -> diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs index d59d7c5635..7631d5c86e 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/LFMBTree.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -21,8 +22,11 @@ import Concordium.Types.ProtocolVersion import Control.Monad import Control.Monad.IO.Class import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16.Lazy as B16 +import Data.Either import Data.IORef import qualified Data.Serialize as S +import Data.String import Data.Word import Test.Hspec import Test.QuickCheck @@ -126,6 +130,66 @@ testTraverseWhileDescRef ws = forAll (chooseBoundedIntegral (0, length ws)) $ \n else liftIO $ expect' `shouldBe` Nothing ) +newtype StringValue = StringValue String + deriving newtype (S.Serialize, Show, IsString, Eq) + +instance (MonadBlobStore m) => BlobStorable m StringValue + +instance (Monad m) => MHashableTo m H.Hash StringValue + +instance HashableTo H.Hash StringValue where + getHash val = H.hashLazy $ S.runPutLazy $ do + S.put val + +-- | Asserts "shapshots" of hashes to make sure they don't change. +snapshotTestHash :: IO () +snapshotTestHash = do + mbs <- newMemBlobStore + flip + runMemBlobStoreT + mbs + ( do + -- Empty tree + let emptyTree = empty :: LFMBTree Word64 HashedBufferedRef StringValue + (h1 :: LFMBTreeHashV1) <- getHashM emptyTree + liftIO $ show h1 `shouldBe` "c423f9e91ee218b2b5303485dd87a3093a653ddb9bdb839d30aa1924de1dbf05" + + -- Tree with values A, B, C + simpleTree <- foldM (\acc v -> snd <$> append v acc) (empty :: LFMBTree Word64 HashedBufferedRef StringValue) ["A", "B", "C"] + (h2 :: LFMBTreeHashV1) <- getHashM simpleTree + liftIO $ show h2 `shouldBe` "b9cac19f6048ef301f586e7e0faa6c08b6012d4b100703eef5dc1fcb26c1ecd5" + ) + +-- | Load trees from storage fixtures, to make sure we stay compatible. +fixtureTestLoad :: IO () +fixtureTestLoad = do + mbs1 <- newMemBlobStoreWithBytes $ fromRight undefined $ B16.decode "00000000000000080000000000000000" + flip + runMemBlobStoreT + mbs1 + ( do + -- Empty tree + (emptyTree :: LFMBTree Word64 HashedBufferedRef StringValue) <- + loadDirect $ BlobRef 0 + liftIO $ size emptyTree `shouldBe` 0 + ) + mbs2 <- newMemBlobStoreWithBytes $ fromRight undefined $ B16.decode "0000000000000009000000000000000141000000000000000900000000000000000000000000000000090000000000000001420000000000000009000000000000000022000000000000001901000000000000000000000000000000110000000000000033000000000000000900000000000000014300000000000000090000000000000000650000000000000021000000000000000301000000000000000100000000000000440000000000000076" + flip + runMemBlobStoreT + mbs2 + ( do + -- Tree with values A, B, C + (simpleTree :: LFMBTree Word64 HashedBufferedRef StringValue) <- + loadDirect $ BlobRef 135 + liftIO $ size simpleTree `shouldBe` 3 + val0 <- lookup 0 simpleTree + liftIO $ val0 `shouldBe` Just "A" + val1 <- lookup 1 simpleTree + liftIO $ val1 `shouldBe` Just "B" + val2 <- lookup 2 simpleTree + liftIO $ val2 `shouldBe` Just "C" + ) + tests :: Spec tests = describe "GlobalStateTests.LFMBTree" $ do @@ -141,4 +205,10 @@ tests = it "testHashAsLFMBTV1" testHashAsLFMBTV1 + it + "snapshotTestHash" + snapshotTestHash + it + "fixtureTestLoad" + fixtureTestLoad it "testTraverseWhileDescRef" $ property testTraverseWhileDescRef diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentChainParameters.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentChainParameters.hs new file mode 100644 index 0000000000..eb4bb45481 --- /dev/null +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentChainParameters.hs @@ -0,0 +1,216 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MonoLocalBinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module GlobalStateTests.PersistentChainParameters (tests) where + +import Control.Exception (ErrorCall, evaluate, try) +import Control.Monad.IO.Class +import Control.Monad.Trans.Maybe +import qualified Data.Serialize as S +import qualified Data.Set as Set +import Lens.Micro.Platform +import Test.HUnit +import Test.Hspec + +import qualified Concordium.Crypto.SHA256 as H +import Concordium.Genesis.Data +import qualified Concordium.Genesis.Data.P11 as P11 +import Concordium.GlobalState.DummyData +import Concordium.GlobalState.Persistent.BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState as PBS +import qualified Concordium.GlobalState.Persistent.BlockState.Parameters as PCP +import qualified Concordium.GlobalState.Persistent.BlockState.Updates as PU +import qualified Concordium.GlobalState.Persistent.Migration as Migration +import Concordium.Types +import Concordium.Types.HashableTo +import Concordium.Types.Parameters +import qualified Concordium.Types.UpdateQueues as UQ +import Concordium.Types.Updates (AccessStructure (..)) + +import GlobalStateTests.BlockStateHelpers (dummySeedState, runTestBlockState) + +-- | Run an action in the 'MemBlobStoreT' monad transformer from an empty store. +runWithNewMemBlobStore :: MemBlobStoreT IO a -> IO a +runWithNewMemBlobStore a = do + mbs <- newMemBlobStore + runMemBlobStoreT a mbs + +-- | The historical chain-parameter layout used for persistent storage before +-- node-owned persistent chain parameters were split from the public view. +-- +-- This deliberately omits '_cpMaxLockDuration'. +newtype OldPersistentChainParametersLayout cpv = OldPersistentChainParametersLayout (ChainParameters' cpv) + +-- | Serialize the historical persistent chain-parameter layout. +putOldPersistentChainParametersLayout :: forall cpv. (IsChainParametersVersion cpv) => S.Putter (ChainParameters' cpv) +putOldPersistentChainParametersLayout ChainParameters{..} = do + withIsConsensusParametersVersionFor (chainParametersVersion @cpv) $ S.put _cpConsensusParameters + S.put _cpExchangeRates + putCooldownParameters _cpCooldownParameters + S.put _cpTimeParameters + S.put _cpAccountCreationLimit + S.put _cpRewardParameters + S.put _cpFoundationAccount + putPoolParameters _cpPoolParameters + S.put _cpFinalizationCommitteeParameters + S.put _cpValidatorScoreParameters + +instance (MonadBlobStore m, IsChainParametersVersion cpv) => BlobStorable m (OldPersistentChainParametersLayout cpv) where + storeUpdate (OldPersistentChainParametersLayout chainParameters) = + return (putOldPersistentChainParametersLayout chainParameters, OldPersistentChainParametersLayout chainParameters) + load = error "OldPersistentChainParametersLayout is only used for writing compatibility test blobs" + +-- | Store bytes in the historical layout and load them as the new persistent type. +loadOldLayoutAsPersistent :: + forall pv. + (IsProtocolVersion pv) => + ChainParameters pv -> + MemBlobStoreT IO (PCP.PersistentChainParameters pv) +loadOldLayoutAsPersistent chainParameters = do + oldRef <- storeRef (OldPersistentChainParametersLayout chainParameters) + loadRef (BlobRef (theBlobRef oldRef)) + +-- | Assert that old-layout bytes load as persistent chain parameters and convert +-- back to the public view without changing ordinary fields. +assertOldLayoutLoadsAsPersistent :: + forall pv. + (IsProtocolVersion pv) => + ChainParameters pv -> + Assertion +assertOldLayoutLoadsAsPersistent chainParameters = runWithNewMemBlobStore $ do + persistent <- loadOldLayoutAsPersistent @pv chainParameters + let publicView = PCP.persistentChainParametersToChainParameters persistent + liftIO $ assertEqual "old persistent layout should load as the new persistent type" chainParameters publicView + +-- | Assert that hashing pre-P11 persistent chain parameters is unchanged from +-- hashing the historical persistent byte layout. +assertOldLayoutHashCompatible :: + forall pv. + (IsProtocolVersion pv) => + ChainParameters pv -> + Assertion +assertOldLayoutHashCompatible chainParameters = runWithNewMemBlobStore $ do + persistent <- loadOldLayoutAsPersistent @pv chainParameters + persistentHash <- getHashM persistent + let oldHash = H.hash (S.runPut (putOldPersistentChainParametersLayout chainParameters)) + liftIO $ assertEqual "persistent chain-parameter hash should match the historical layout hash" oldHash persistentHash + +p10ChainParameters :: ChainParameters' 'ChainParametersV3 +p10ChainParameters = dummyChainParameters' & cpMaxLockDuration .~ SomeParam Nothing + +p11ChainParameters :: Duration -> ChainParameters' 'ChainParametersV3 +p11ChainParameters duration = dummyChainParameters' & cpMaxLockDuration .~ SomeParam (Just duration) + +p11ProtocolUpdateData :: Duration -> P11.ProtocolUpdateData +p11ProtocolUpdateData duration = + P11.ProtocolUpdateData + { P11.updateTokenParametersAccessStructure = AccessStructure (Set.singleton 0) 1, + P11.updateMaxLockDuration = duration + } + +assertP10P11MigrationExposesMaxLockDuration :: Assertion +assertP10P11MigrationExposesMaxLockDuration = runWithNewMemBlobStore $ do + let duration = Duration 12345 + migration = StateMigrationParametersP10ToP11 (P11.StateMigrationData (p11ProtocolUpdateData duration)) + persistent0 <- PCP.makePersistentChainParameters @(MemBlobStoreT IO) @'P10 p10ChainParameters + migratedMaybe <- runMaybeT $ Migration.migrateChainParameters migration persistent0 + migrated <- case migratedMaybe of + Nothing -> liftIO $ assertFailure "P10-to-P11 chain-parameter migration unexpectedly failed" + Just migrated -> return migrated + publicView <- PCP.persistentChainParametersToChainParametersM migrated + liftIO $ assertEqual "P10-to-P11 migration should expose protocol-update maxLockDuration" (SomeParam (Just duration)) (publicView ^. cpMaxLockDuration) + +assertP11InitialPersistentStateExposesMaxLockDuration :: Assertion +assertP11InitialPersistentStateExposesMaxLockDuration = runTestBlockState @'P11 $ do + let duration = Duration 67890 + hpbs <- + PBS.initialPersistentState @'P11 + (dummySeedState SP11) + dummyCryptographicParameters + [] + dummyIdentityProviders + dummyArs + (dummyKeyCollection @'AuthorizationsVersion3) + (p11ChainParameters duration) + bsp <- PBS.loadPBS (PBS.hpbsPointers hpbs) + updates <- refLoad (PBS.bspUpdates bsp) + basicUpdates <- PU.makeBasicUpdates updates + liftIO $ assertEqual "P11 initial state should expose genesis maxLockDuration" (SomeParam (Just duration)) (UQ._currentParameters basicUpdates ^. cpMaxLockDuration) + +assertP11InitialPersistentStateRequiresMaxLockDuration :: Assertion +assertP11InitialPersistentStateRequiresMaxLockDuration = do + result <- try $ runTestBlockState @'P11 $ do + hpbs <- + PBS.initialPersistentState @'P11 + (dummySeedState SP11) + dummyCryptographicParameters + [] + dummyIdentityProviders + dummyArs + (dummyKeyCollection @'AuthorizationsVersion3) + p10ChainParameters + liftIO $ evaluate hpbs + case result of + Left (_ :: ErrorCall) -> return () + Right _ -> assertFailure "P11 initial persistent state should require maxLockDuration" + +assertP11RoundtripExposesMaxLockDuration :: Assertion +assertP11RoundtripExposesMaxLockDuration = runWithNewMemBlobStore $ do + let duration = Duration 42 + persistent0 <- PCP.makePersistentChainParameters @(MemBlobStoreT IO) @'P11 (p11ChainParameters duration) + (hash0 :: H.Hash) <- getHashM persistent0 + persistent1 <- loadRef =<< storeRef persistent0 + (hash1 :: H.Hash) <- getHashM persistent1 + persistent2 <- cache persistent1 + publicView <- PCP.persistentChainParametersToChainParametersM persistent2 + liftIO $ do + assertEqual "P11 maxLockDuration should survive store/load/cache" (SomeParam (Just duration)) (publicView ^. cpMaxLockDuration) + assertEqual "P11 persistent chain-parameter hash should survive store/load" hash0 hash1 + +assertP11HashIncludesExternalChainParameters :: Assertion +assertP11HashIncludesExternalChainParameters = runWithNewMemBlobStore $ do + persistent1 <- PCP.makePersistentChainParameters @(MemBlobStoreT IO) @'P11 (p11ChainParameters (Duration 1)) + persistent2 <- PCP.makePersistentChainParameters @(MemBlobStoreT IO) @'P11 (p11ChainParameters (Duration 2)) + (hash1 :: H.Hash) <- getHashM persistent1 + (hash2 :: H.Hash) <- getHashM persistent2 + liftIO $ assertBool "P11 persistent chain-parameter hash should include external maxLockDuration" (hash1 /= hash2) + +assertP11ConstructionRequiresMaxLockDuration :: Assertion +assertP11ConstructionRequiresMaxLockDuration = do + result <- try $ runWithNewMemBlobStore $ do + persistent <- PCP.makePersistentChainParameters @(MemBlobStoreT IO) @'P11 p10ChainParameters + liftIO $ evaluate persistent + case result of + Left (_ :: ErrorCall) -> return () + Right _ -> assertFailure "P11 persistent chain-parameter construction should require maxLockDuration" + +tests :: Spec +tests = describe "GlobalStateTests.PersistentChainParameters" $ do + it "loads old CPV0 persistent bytes as node-owned persistent chain parameters" $ + assertOldLayoutLoadsAsPersistent @'P1 dummyChainParameters' + it "loads old CPV1 persistent bytes as node-owned persistent chain parameters" $ + assertOldLayoutLoadsAsPersistent @'P4 dummyChainParameters' + it "loads old CPV2 persistent bytes as node-owned persistent chain parameters" $ + assertOldLayoutLoadsAsPersistent @'P6 dummyChainParameters' + it "loads old pre-P11 CPV3 persistent bytes as node-owned persistent chain parameters" $ + assertOldLayoutLoadsAsPersistent @'P10 p10ChainParameters + it "keeps old pre-P11 persistent chain-parameter hashes unchanged" $ do + assertOldLayoutHashCompatible @'P1 dummyChainParameters' + assertOldLayoutHashCompatible @'P4 dummyChainParameters' + assertOldLayoutHashCompatible @'P6 dummyChainParameters' + assertOldLayoutHashCompatible @'P10 p10ChainParameters + it "migrates P10 chain parameters to P11 with protocol-update maxLockDuration" $ + assertP10P11MigrationExposesMaxLockDuration + it "initializes P11 persistent state with genesis maxLockDuration" $ + assertP11InitialPersistentStateExposesMaxLockDuration + it "rejects P11 initial persistent state without maxLockDuration" $ + assertP11InitialPersistentStateRequiresMaxLockDuration + it "stores, loads, caches, hashes, and exposes P11 external maxLockDuration" $ + assertP11RoundtripExposesMaxLockDuration + it "includes P11 external maxLockDuration in the persistent chain-parameter hash" $ + assertP11HashIncludesExternalChainParameters + it "rejects P11 persistent construction without maxLockDuration" $ + assertP11ConstructionRequiresMaxLockDuration diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs index f23e3b4ee9..864a20e3d9 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/PersistentTreeState.hs @@ -78,7 +78,7 @@ createGlobalState dbDir = do accountMap <- LMDBAccountMap.openDatabase (dbDir "accountmap") let n = 3 - genesis = makeTestingGenesisDataP5 now n 1 1 dummyFinalizationCommitteeMaxSize dummyCryptographicParameters emptyIdentityProviders emptyAnonymityRevokers maxBound dummyKeyCollection dummyChainParameters + genesis = makeTestingGenesisDataP5 now n 1 1 dummyFinalizationCommitteeMaxSize dummyCryptographicParameters emptyIdentityProviders emptyAnonymityRevokers maxBound dummyKeyCollection (dummyChainParameters @PV) config = GlobalStateConfig defaultRuntimeParameters dbDir (dbDir "blockstate" <.> "dat") accountMap (x, y) <- runSilentLogger $ initialiseGlobalState genesis config return (x, y) diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/ProtocolLevelTokens.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/ProtocolLevelTokens.hs index a511c2d926..a415daf616 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/ProtocolLevelTokens.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/ProtocolLevelTokens.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} module GlobalStateTests.ProtocolLevelTokens where @@ -11,19 +12,28 @@ import Test.QuickCheck as QuickCheck import qualified Concordium.Crypto.SHA256 as SHA256 import Concordium.Types +import Concordium.Types.Conditionally import Concordium.Types.HashableTo import Concordium.Types.Tokens +import Concordium.Crypto.SHA256 import Concordium.GlobalState.Persistent.BlobStore import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens +import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState +import Control.Monad.IO.Class +import qualified Data.ByteString as BS +import qualified Data.ByteString.Base16.Lazy as B16 +import Data.Either +import qualified Data.FixedByteString as FBS +import Data.Maybe -- | Generate a 'TokenRawAmount' value. genTokenRawAmount :: Gen TokenRawAmount genTokenRawAmount = TokenRawAmount <$> arbitrary -- | Run an action in the 'MemBlobStoreT' monad transformer from an empty store. -runBlobStore :: MemBlobStoreT IO a -> IO a -runBlobStore a = do +runWithNewMemBlobStore :: MemBlobStoreT IO a -> IO a +runWithNewMemBlobStore a = do mbs <- newMemBlobStore runMemBlobStoreT a mbs @@ -53,7 +63,7 @@ emptyPLTPV :: (MonadBlobStore m) => m (ProtocolLevelTokensForPV 'P9) emptyPLTPV = emptyProtocolLevelTokensForPV testCreateToken :: Assertion -testCreateToken = runBlobStore $ do +testCreateToken = runWithNewMemBlobStore $ do (idx, tokens) <- createToken configABC =<< emptyPLTPV checks0 idx tokens (idx', tokens') <- createToken configDEF tokens @@ -87,7 +97,7 @@ testCreateToken = runBlobStore $ do =<< getTokenConfiguration idx' tokens' testSetTokenCirculatingSupply :: Assertion -testSetTokenCirculatingSupply = runBlobStore $ do +testSetTokenCirculatingSupply = runWithNewMemBlobStore $ do (idxABC, tokens0) <- createToken configABC =<< emptyPLTPV (idxDEF, tokens1) <- createToken configDEF tokens0 tokens2 <- setTokenCirculatingSupply idxABC 100 tokens1 @@ -95,13 +105,13 @@ testSetTokenCirculatingSupply = runBlobStore $ do =<< getTokenCirculatingSupply idxABC tokens2 lift . assertEqual "getTokenCirculatingSupply for DEF returns expected amount" 0 =<< getTokenCirculatingSupply idxDEF tokens2 - hash2 <- getHashM @_ @ProtocolLevelTokensHash tokens2 + (hash2 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokens2 tokens3 <- setTokenCirculatingSupply idxDEF 200 tokens2 lift . assertEqual "getTokenCirculatingSupply for ABC returns expected amount" 100 =<< getTokenCirculatingSupply idxABC tokens3 lift . assertEqual "getTokenCirculatingSupply for DEF returns expected amount" 200 =<< getTokenCirculatingSupply idxDEF tokens3 - hash3 <- getHashM @_ @ProtocolLevelTokensHash tokens3 + (hash3 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokens3 lift $ assertBool "Hash of tokens2 and tokens3 should be different" (hash2 /= hash3) tokens4 <- setTokenCirculatingSupply idxABC 0 tokens3 lift . assertEqual "getTokenCirculatingSupply for ABC returns expected amount" 0 @@ -118,16 +128,16 @@ testSetTokenCirculatingSupply = runBlobStore $ do =<< getTokenCirculatingSupply idxABC tokens6 lift . assertEqual "getTokenCirculatingSupply for DEF returns expected amount" 200 =<< getTokenCirculatingSupply idxDEF tokens6 - hash6 <- getHashM tokens6 + hash6 <- uncond <$> getHashM tokens6 lift $ assertEqual "Hash of tokens3 and tokens6 should be the same" hash3 hash6 testUpdateTokenState :: Assertion -testUpdateTokenState = runBlobStore $ do +testUpdateTokenState = runWithNewMemBlobStore $ do (idxABC, tokens0) <- createToken configABC =<< emptyPLTPV (idxDEF, tokens1) <- createToken configDEF tokens0 mutableStateABC <- getMutableTokenState idxABC tokens1 mutableStateDEF <- getMutableTokenState idxDEF tokens1 - hash1 <- getHashM @_ @ProtocolLevelTokensHash tokens1 + (hash1 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokens1 lift . assertEqual "lookupTokenState for ABC \"TestKey1\"" Nothing =<< lookupTokenState "TestKey1" mutableStateABC @@ -155,9 +165,9 @@ testUpdateTokenState = runBlobStore $ do tokens2 <- setTokenState idxABC mutableStateABC tokens1 >>= setTokenState idxDEF mutableStateDEF - hash2 <- getHashM @_ @ProtocolLevelTokensHash tokens2 + (hash2 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokens2 lift $ assertBool "Hash of tokens1 and tokens2 should not be the same" (hash1 /= hash2) - hash1' <- getHashM @_ @ProtocolLevelTokensHash tokens1 + hash1' <- uncond <$> getHashM tokens1 lift $ assertEqual "Hash of tokens1 should stay the same" hash1 hash1' mutableStateABC2 <- getMutableTokenState idxABC tokens2 @@ -183,11 +193,113 @@ testUpdateTokenState = runBlobStore $ do tokens3 <- setTokenState idxABC mutableStateABC2 tokens2 >>= setTokenState idxDEF mutableStateDEF2 - hash3 <- getHashM @_ @ProtocolLevelTokensHash tokens3 + hash3 <- uncond <$> getHashM tokens3 lift $ assertEqual "Hash of tokens2 and tokens3 should be the same" hash2 hash3 +-- | Asserts "shapshot" of hash of empty 'ProtocolLevelTokens' to make sure it does not change. +snapshotTestHashEmpty :: Assertion +snapshotTestHashEmpty = runWithNewMemBlobStore $ do + tokensState <- emptyPLTPV + + -- Assert hash + (hash1 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokensState + liftIO $ show hash1 `shouldBe` "c423f9e91ee218b2b5303485dd87a3093a653ddb9bdb839d30aa1924de1dbf05" + +-- | Asserts "shapshot" of hash of 'ProtocolLevelTokens' with some simple PLTs to make sure it does not change. +snapshotTestHashSimple :: Assertion +snapshotTestHashSimple = runWithNewMemBlobStore $ do + tokensState1 <- emptyPLTPV + + -- Create tokens + let config1 = + PLTConfiguration + { _pltTokenId = TokenId "token1", + _pltModule = TokenModuleRef $ Hash $ FBS.pack (replicate 32 5), + _pltDecimals = 2 + } + (tokenIndex1, tokensState2) <- createToken config1 tokensState1 + tokensState3 <- setTokenCirculatingSupply tokenIndex1 100 tokensState2 + mutableKeyValueState1 <- getMutableTokenState tokenIndex1 tokensState3 + _ <- updateTokenState (BS.pack [0, 1]) (Just (BS.pack [0, 0])) mutableKeyValueState1 + _ <- updateTokenState (BS.pack [0, 2]) (Just (BS.pack [1, 1])) mutableKeyValueState1 + tokensState4 <- setTokenState tokenIndex1 mutableKeyValueState1 tokensState3 + let config2 = + PLTConfiguration + { _pltTokenId = TokenId "token2", + _pltModule = TokenModuleRef $ Hash $ FBS.pack (replicate 32 5), + _pltDecimals = 4 + } + (_tokenIndex2, tokensState5) <- createToken config2 tokensState4 + + -- Assert hash + (hash1 :: ProtocolLevelTokensHash) <- uncond <$> getHashM tokensState5 + liftIO $ show hash1 `shouldBe` "d202e9153fea3fdd22c594be21d471c07e9619abc0baad3faca5c81f0bb1504b" + +-- | Load empty PLTs state from storage fixture, to make sure we stay compatible. +fixtureTestLoadEmpty :: Assertion +fixtureTestLoadEmpty = runWithNewMemBlobStore $ do + mbs1 <- liftIO $ newMemBlobStoreWithBytes $ fromRight undefined $ B16.decode "00000000000000080000000000000000" + flip + runMemBlobStoreT + mbs1 + ( do + -- Load empty PLTs state + (emptyStateV0 :: ProtocolLevelTokens) <- + loadDirect $ BlobRef 0 + emptyState <- ProtocolLevelTokensV0 @'P9 <$> refMake emptyStateV0 + + -- Assert empty + pltList <- getPLTList emptyState + liftIO $ assertEqual "PLTList" (length pltList) 0 + ) + +-- | Load PLTs state with some simple PLTs from storage fixture, to make sure we stay compatible. +fixtureTestLoadSimple :: Assertion +fixtureTestLoadSimple = runWithNewMemBlobStore $ do + mbs1 <- liftIO $ newMemBlobStoreWithBytes $ fromRight undefined $ B16.decode "000000000000002806746f6b656e310505050505050505050505050505050505050505050505050505050505050505020000000000000025edbda48b85971b3a874334ca94f07e55e6a6e63eabca968d1257a3223e1b84e14002010100000000000000002503b0eab929105fd6df1ec793cbaf1b554a7a385520a9f7c902adf0219ace6dab4002000000000000000000003648b07111a93452374c7bcf66ee01959af6b4a52cb7cd299341e9ea77b378b0230300000201000000000000005d020000000000000030000000000000000901000000000000008a0000000000000011000000000000000000000000000000c86400000000000000090000000000000000d9000000000000002806746f6b656e3205050505050505050505050505050505050505050505050505050505050505050400000000000000010000000000000000110000000000000103000000000000013300000000000000000900000000000000013c0000000000000021000000000000000201000000000000000000000000000000f20000000000000155" + flip + runMemBlobStoreT + mbs1 + ( do + -- Load simple PLTs state + (simpleStateV0 :: ProtocolLevelTokens) <- + loadDirect $ BlobRef 358 + simpleState <- ProtocolLevelTokensV0 @'P9 <$> refMake simpleStateV0 + + -- Assert token state + pltList <- getPLTList simpleState + liftIO $ assertEqual "PLTList" (length pltList) 2 + let expectedConfig1 = + PLTConfiguration + { _pltTokenId = TokenId "token1", + _pltModule = TokenModuleRef $ Hash $ FBS.pack (replicate 32 5), + _pltDecimals = 2 + } + tokenIndex1 <- fromJust <$> getTokenIndex (TokenId "token1") simpleState + config1 <- getTokenConfiguration tokenIndex1 simpleState + liftIO $ assertEqual "config1" config1 expectedConfig1 + keyValueState1 <- getMutableTokenState tokenIndex1 simpleState + value1 <- lookupTokenState (BS.pack [0, 1]) keyValueState1 + liftIO $ assertEqual "value1" value1 (Just $ BS.pack [0, 0]) + value2 <- lookupTokenState (BS.pack [0, 2]) keyValueState1 + liftIO $ assertEqual "value1" value2 (Just $ BS.pack [1, 1]) + let expectedConfig2 = + PLTConfiguration + { _pltTokenId = TokenId "token2", + _pltModule = TokenModuleRef $ Hash $ FBS.pack (replicate 32 5), + _pltDecimals = 4 + } + tokenIndex2 <- fromJust <$> getTokenIndex (TokenId "token2") simpleState + config2 <- getTokenConfiguration tokenIndex2 simpleState + liftIO $ assertEqual "config2" config2 expectedConfig2 + ) + tests :: Spec tests = describe "GlobalStateTests.ProtocolLevelTokens" $ do it "createToken" testCreateToken it "setTokenCirculatingSupply" testSetTokenCirculatingSupply it "updateTokenState" testUpdateTokenState + it "snapshotHashEmpty" snapshotTestHashEmpty + it "snapshotHashSimple" snapshotTestHashSimple + it "fixtureLoadEmpty" fixtureTestLoadEmpty + it "fixtureLoadSimple" fixtureTestLoadSimple diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/RustPLTBlockState.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/RustPLTBlockState.hs new file mode 100644 index 0000000000..8b95ad2aa5 --- /dev/null +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/RustPLTBlockState.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +-- | Tests for the Rust-maintained PLT block state primarily to smoke test +-- the FFI interface. +module GlobalStateTests.RustPLTBlockState (tests) where + +import Control.Monad.IO.Class +import Test.HUnit +import Test.Hspec + +import Concordium.Types +import Concordium.Types.HashableTo + +import Concordium.GlobalState.Persistent.BlobStore +import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.RustPLTBlockState as RustPLT + +-- | Run an action in the 'MemBlobStoreT' monad transformer from an empty store. +runWithNewMemBlobStore :: MemBlobStoreT IO a -> IO a +runWithNewMemBlobStore a = do + mbs <- newMemBlobStore + runMemBlobStoreT a mbs + +-- | Test store, load, cache and hash operations +testStoreLoadHashCache :: Assertion +testStoreLoadHashCache = runWithNewMemBlobStore $ do + -- Create empty state + (state :: ForeignPLTBlockStatePtr 'P11) <- RustPLT.empty + (hashBefore :: ProtocolLevelTokensHash) <- getHashM state + -- Store and load state + (loaded :: ForeignPLTBlockStatePtr 'P11) <- loadRef =<< storeRef state + (hashAfter :: ProtocolLevelTokensHash) <- getHashM loaded + liftIO $ assertEqual "Hash should be preserved across store/load" hashBefore hashAfter + -- Cache state + (_cached :: ForeignPLTBlockStatePtr 'P11) <- cache state + return () + +-- | Test migrate state. +testMigrate :: Assertion +testMigrate = do + sourceStore <- newMemBlobStore + flip runMemBlobStoreT sourceStore $ do + -- Create empty + (state :: ForeignPLTBlockStatePtr 'P10) <- RustPLT.empty + + -- Migrate + targetStore <- liftIO newMemBlobStore + flip runMemBlobStoreT targetStore $ do + (_migrated :: ForeignPLTBlockStatePtr 'P11) <- migrate state + return () + +tests :: Spec +tests = describe "GlobalStateTests.RustPLTBlockState" $ do + it "smoke test store, load, hash and cache FFI calls" testStoreLoadHashCache + it "smoke test migrate FFI call" testMigrate diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/UpdateQueues.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/UpdateQueues.hs index 81633913e5..6af709f432 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/UpdateQueues.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/UpdateQueues.hs @@ -15,24 +15,25 @@ import Test.Hspec import Concordium.GlobalState.DummyData import Concordium.GlobalState.Parameters import Concordium.GlobalState.Persistent.BlobStore +import qualified Concordium.GlobalState.Persistent.BlockState.Parameters as PCP import qualified Concordium.GlobalState.Persistent.BlockState.Updates as PU import Concordium.Types -- This tests that chain parameter updates that are scheduled at the same time are not lost -- when calling 'PU.processUpdateQueues'. -testCase :: forall cpv auv. (IsChainParametersVersion cpv, IsAuthorizationsVersion auv) => SChainParametersVersion cpv -> SAuthorizationsVersion auv -> String -> IO () -testCase _ _ pvString = do +testCase :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> String -> IO () +testCase _ pvString = do -- Schedule three updates let rootKeyUpdate = UVRootKeys dummyHigherLevelKeys - let poolParameterUpdate = UVPoolParameters (dummyChainParameters @cpv ^. cpPoolParameters) - let euroEnergyExchange = UVEuroPerEnergy (_erEuroPerEnergy (dummyChainParameters @cpv ^. cpExchangeRates)) + let poolParameterUpdate = UVPoolParameters (dummyChainParameters' @(ChainParametersVersionFor pv) ^. cpPoolParameters) + let euroEnergyExchange = UVEuroPerEnergy (_erEuroPerEnergy (dummyChainParameters' @(ChainParametersVersionFor pv) ^. cpExchangeRates)) -- The first two are scheduled at effectiveTime = 123 -- The last one is schedule for a millisecond earlier. let effectiveTime = 123 :: TransactionTime effects <- liftIO . runBlobStoreTemp "." $ do - (u1 :: BufferedRef (PU.Updates' cpv auv)) <- + (u1 :: BufferedRef (PU.Updates pv)) <- refMake - =<< PU.initialUpdates dummyKeyCollection dummyChainParameters + =<< PU.initialUpdates (dummyKeyCollection @(AuthorizationsVersionFor pv)) (dummyChainParameters' @(ChainParametersVersionFor pv)) enqueuedState <- PU.enqueueUpdate effectiveTime poolParameterUpdate =<< PU.enqueueUpdate (effectiveTime - 1) euroEnergyExchange @@ -48,10 +49,49 @@ testCase _ _ pvString = do ] effects +testMaxLockDurationUpdate :: IO () +testMaxLockDurationUpdate = do + let effectiveTime = 123 :: TransactionTime + newDuration = Duration 123456 + update :: UpdateValue 'ChainParametersV3 'AuthorizationsVersion3 + update = UVMaxLockDuration newDuration + (effects, oldMaxLockDuration, newMaxLockDuration) <- liftIO . runBlobStoreTemp "." $ do + (u0 :: BufferedRef (PU.Updates 'P11)) <- + refMake + =<< PU.initialUpdates + (dummyKeyCollection @'AuthorizationsVersion3) + (dummyChainParameters @'P11) + originalParametersRef <- PU.currentParameters <$> refLoad u0 + u1 <- PU.enqueueUpdate effectiveTime update u0 + ars <- refMake dummyArs + ips <- refMake dummyIdentityProviders + (processedEffects, (u2, _, _)) <- PU.processUpdateQueues (transactionTimeToTimestamp effectiveTime) (u1, ars, ips) + updatedParametersRef <- PU.currentParameters <$> refLoad u2 + originalParameters <- PCP.persistentChainParametersToChainParametersM =<< refLoad originalParametersRef + updatedParameters <- PCP.persistentChainParametersToChainParametersM =<< refLoad updatedParametersRef + return + ( processedEffects, + originalParameters ^. cpMaxLockDuration, + updatedParameters ^. cpMaxLockDuration + ) + assertEqual + "The max lock duration update should be returned" + [(effectiveTime, update)] + effects + assertEqual + "The original chain parameters should remain unchanged" + (dummyChainParameters @'P11 ^. cpMaxLockDuration) + oldMaxLockDuration + assertEqual + "The public chain-parameter view should expose the updated max lock duration" + (SomeParam (Just newDuration)) + newMaxLockDuration + tests :: Spec tests = do describe "Scheduler.UpdateQueues" $ do specify "Correct effects are returned" $ do - testCase SChainParametersV0 SAuthorizationsVersion0 "CPV0" - testCase SChainParametersV1 SAuthorizationsVersion1 "CPV1" - testCase SChainParametersV2 SAuthorizationsVersion1 "CPV2" + testCase SP1 "CPV0" + testCase SP4 "CPV1" + testCase SP6 "CPV2" + specify "Effective max lock duration updates create new external chain parameters" testMaxLockDurationUpdate diff --git a/concordium-consensus/tests/globalstate/GlobalStateTests/Updates.hs b/concordium-consensus/tests/globalstate/GlobalStateTests/Updates.hs index acaa5e7161..758f6d64ec 100644 --- a/concordium-consensus/tests/globalstate/GlobalStateTests/Updates.hs +++ b/concordium-consensus/tests/globalstate/GlobalStateTests/Updates.hs @@ -39,6 +39,7 @@ import Concordium.Crypto.DummyData import qualified Concordium.Crypto.SHA256 as Hash import qualified Concordium.Crypto.VRF as VRF import Concordium.GlobalState.BakerInfo +import qualified Concordium.GlobalState.Persistent.BlockState.Parameters as PCP import qualified Concordium.GlobalState.Persistent.BlockState.Updates as PU import Concordium.ID.DummyData import Concordium.ID.Parameters @@ -58,6 +59,7 @@ newtype NoLoggerT m a = NoLoggerT {runNoLoggerT :: m a} instance (Monad m) => MonadLogger (NoLoggerT m) where logEvent _ _ _ = return () + logEventIO = return $ \_ _ _ -> return () type PV = 'P5 @@ -84,7 +86,7 @@ createGS = do dummyIdentityProviders dummyArs dummyKeyCollection - dummyChainParameters + (dummyChainParameters @PV) -------------------------------------------------------------------------------- -- -- @@ -93,7 +95,7 @@ createGS = do -------------------------------------------------------------------------------- limit :: Amount -limit = dummyChainParameters @'ChainParametersV0 ^. cpPoolParameters . ppBakerStakeThreshold +limit = dummyChainParameters' @'ChainParametersV0 ^. cpPoolParameters . ppBakerStakeThreshold limitDelta :: AmountDelta limitDelta = fromIntegral limit @@ -182,9 +184,10 @@ increaseLimit newLimit (bs2, ai) = do -- load the updates field updates <- refLoad (PBS.bspUpdates bsp) -- load the current parameters - currentParams <- unStoreSerialized <$> refLoad (PU.currentParameters updates) + currentPersistentParams <- refLoad (PU.currentParameters updates) + let currentParams = PCP.persistentChainParametersToChainParameters currentPersistentParams -- store the new parameters - newParams <- refMake $ StoreSerialized (currentParams & cpPoolParameters . ppMinimumEquityCapital .~ newLimit) + newParams <- refMake $ PCP.updateChainParameters (currentParams & cpPoolParameters . ppMinimumEquityCapital .~ newLimit) currentPersistentParams -- store the new updates newUpdates <- refMake (updates{PU.currentParameters = newParams}) -- store the new block in the IORef diff --git a/concordium-consensus/tests/globalstate/Spec.hs b/concordium-consensus/tests/globalstate/Spec.hs index c3436c9ec8..c14ac9ffb3 100644 --- a/concordium-consensus/tests/globalstate/Spec.hs +++ b/concordium-consensus/tests/globalstate/Spec.hs @@ -22,8 +22,10 @@ import qualified GlobalStateTests.FinalizationSerializationSpec (tests) import qualified GlobalStateTests.Instances (tests) import qualified GlobalStateTests.LFMBTree (tests) import qualified GlobalStateTests.LMDBAccountMap (tests) +import qualified GlobalStateTests.PersistentChainParameters (tests) import qualified GlobalStateTests.PersistentTreeState (tests) import qualified GlobalStateTests.ProtocolLevelTokens (tests) +import qualified GlobalStateTests.RustPLTBlockState (tests) import qualified GlobalStateTests.Trie (tests) import qualified GlobalStateTests.UpdateQueues (tests) import qualified GlobalStateTests.Updates (tests) @@ -48,6 +50,7 @@ main = atLevel $ \lvl -> hspec $ do GlobalStateTests.Accounts.tests lvl GlobalStateTests.Trie.tests GlobalStateTests.PersistentTreeState.tests + GlobalStateTests.PersistentChainParameters.tests GlobalStateTests.FinalizationSerializationSpec.tests GlobalStateTests.Instances.tests lvl GlobalStateTests.AccountReleaseScheduleTest.tests @@ -66,4 +69,5 @@ main = atLevel $ \lvl -> hspec $ do GlobalStateTests.ConfigureValidator.tests lvl GlobalStateTests.ConfigureDelegator.tests GlobalStateTests.ProtocolLevelTokens.tests + GlobalStateTests.RustPLTBlockState.tests GlobalStateTests.Account.tests lvl diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/AccountTransactionSpecs.hs b/concordium-consensus/tests/scheduler/SchedulerTests/AccountTransactionSpecs.hs index 20108382f7..6e3a255e26 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/AccountTransactionSpecs.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/AccountTransactionSpecs.hs @@ -11,7 +11,7 @@ import Test.Hspec import qualified Concordium.ID.Types as Types import qualified Concordium.Scheduler as Sch -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import qualified Concordium.Scheduler.Types as Types import Concordium.TransactionVerification diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/BlockEnergyLimitSpec.hs b/concordium-consensus/tests/scheduler/SchedulerTests/BlockEnergyLimitSpec.hs index f5867ae650..c4a5afdc01 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/BlockEnergyLimitSpec.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/BlockEnergyLimitSpec.hs @@ -11,7 +11,7 @@ import Test.Hspec import qualified Concordium.GlobalState.Persistent.BlockState as BS import qualified Concordium.Scheduler as Sch -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import Concordium.Scheduler.Runner import qualified Concordium.Scheduler.Types as Types diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/ChainMetatest.hs b/concordium-consensus/tests/scheduler/SchedulerTests/ChainMetatest.hs index ea6e37bbff..13d0eb831e 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/ChainMetatest.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/ChainMetatest.hs @@ -8,7 +8,7 @@ import Test.HUnit import Test.Hspec import qualified Concordium.Scheduler as Sch -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import Concordium.Scheduler.Runner import qualified Concordium.Scheduler.Types as Types import Concordium.Wasm (WasmVersion (..)) diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/Helpers.hs b/concordium-consensus/tests/scheduler/SchedulerTests/Helpers.hs index b3209055e3..90bb36abe3 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/Helpers.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/Helpers.hs @@ -56,11 +56,12 @@ import Concordium.GlobalState.Types import Concordium.Logger import Concordium.Scheduler import qualified Concordium.Scheduler.DummyData as DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import qualified Concordium.Scheduler.Runner as SchedTest import qualified Concordium.Scheduler.Types as Types import Concordium.TimeMonad import Concordium.Types (SProtocolVersion) +import qualified Control.Monad.Catch as Catch getResults :: [(a, Types.TransactionSummary tov)] -> [(a, Types.ValidResult)] getResults = map (\(x, r) -> (x, Types.tsResult r)) @@ -99,7 +100,9 @@ newtype PersistentBSM pv a = PersistentBSM Functor, Monad, BlockStateTypes, - MonadIO + MonadIO, + Catch.MonadThrow, + Catch.MonadCatch ) deriving instance (Types.IsProtocolVersion pv) => BS.AccountOperations (PersistentBSM pv) @@ -123,6 +126,7 @@ deriving instance instance MonadLogger (PersistentBSM pv) where logEvent src lvl msg = PersistentBSM (logEvent src lvl msg) + logEventIO = PersistentBSM logEventIO instance TimeMonad (PersistentBSM pv) where currentTime = return $ read "1970-01-01 13:27:13.257285424 UTC" @@ -144,7 +148,8 @@ forEveryProtocolVersion check = check Types.SP7 "P7", check Types.SP8 "P8", check Types.SP9 "P9", - check Types.SP10 "P10" + check Types.SP10 "P10", + check Types.SP11 "P11" ] -- | Convert an energy value to an amount, based on the exchange rates used in @@ -168,7 +173,7 @@ createTestBlockStateWithAccountsAndKeys accounts keys = do DummyData.dummyIdentityProviders DummyData.dummyArs keys - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @pv) -- save block state and accounts. void $ BS.saveBlockState bs void $ BS.saveGlobalMaps bs @@ -806,18 +811,23 @@ assertFailureWithReason expectedReason result = [] -> assertFailure "No transaction failed" other -> assertFailure $ "Multiple transactions failed: " ++ show other --- | Assert the scheduler result has failed one chain update and check the reason. -assertUpdateFailureWithReason :: Types.FailureKind -> SchedulerResult tov -> Assertion -assertUpdateFailureWithReason expectedReason result = +-- | Assert the scheduler result has failed one chain update and check the failure kind. +assertUpdateFailureWhere :: (Types.FailureKind -> Assertion) -> SchedulerResult tov -> Assertion +assertUpdateFailureWhere assertFailureKind result = case ftFailedUpdates $ srTransactions result of - [(_, reason)] -> - assertEqual - "The correct reason for failure is produced" - expectedReason - reason + [(_, failureKind)] -> + assertFailureKind failureKind [] -> assertFailure "No transaction failed" other -> assertFailure $ "Multiple transactions failed: " ++ show other +-- | Assert the scheduler result has failed one chain update and check the reason. +assertUpdateFailureWithReason :: Types.FailureKind -> SchedulerResult tov -> Assertion +assertUpdateFailureWithReason expectedReason = + assertUpdateFailureWhere $ + assertEqual + "The correct reason for failure is produced" + expectedReason + -- | Assert the scheduler have used energy the exact energy needed to deploy a provided V0 smart -- contract module. Assuming the transaction was signed with a single signature. -- The provided module should be a WASM module and without the smart contract version prefix. diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/KonsensusV1/EpochTransition.hs b/concordium-consensus/tests/scheduler/SchedulerTests/KonsensusV1/EpochTransition.hs index 65933e7d81..57024682f7 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/KonsensusV1/EpochTransition.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/KonsensusV1/EpochTransition.hs @@ -337,7 +337,7 @@ makeInitialState :: makeInitialState accs seedState rpLen = withIsAuthorizationsVersionFor (protocolVersion @pv) $ do initialAccounts <- mapM makeDummyAccount accs let chainParams :: ChainParameters pv - chainParams = DummyData.dummyChainParameters & cpTimeParameters . tpRewardPeriodLength .~ rpLen + chainParams = DummyData.dummyChainParameters @pv & cpTimeParameters . tpRewardPeriodLength .~ rpLen initialBS <- initialPersistentState seedState @@ -480,7 +480,7 @@ testEpochTransitionPaydayOnly accountConfigs = runTestBlockState @P7 $ do startEpoch = 10 startTriggerTime = 1000 cooldownDuration = - DummyData.dummyChainParameters @ChainParametersV2 + DummyData.dummyChainParameters' @ChainParametersV2 ^. cpCooldownParameters . cpUnifiedCooldown -- | Test an snapshot epoch transition. @@ -541,7 +541,7 @@ testEpochTransitionSnapshotOnly accountConfigs = runTestBlockState @P7 $ do hour = Duration 3_600_000 startEpoch = 10 startTriggerTime = 1000 - chainParams = DummyData.dummyChainParameters @ChainParametersV2 + chainParams = DummyData.dummyChainParameters' @ChainParametersV2 -- | Test two successive epoch transitions where the first is a snapshot and the second is a payday. testEpochTransitionSnapshotPayday :: [AccountConfig 'AccountV3] -> Assertion @@ -626,7 +626,7 @@ testEpochTransitionSnapshotPayday accountConfigs = runTestBlockState @P7 $ do hour = Duration 3_600_000 startEpoch = 10 startTriggerTime = 1000 - chainParams = DummyData.dummyChainParameters @ChainParametersV2 + chainParams = DummyData.dummyChainParameters' @ChainParametersV2 cooldownDuration = chainParams ^. cpCooldownParameters . cpUnifiedCooldown -- | Test epoch transitions for two successive transitions where the payday length is one epoch. @@ -735,7 +735,7 @@ testEpochTransitionSnapshotPaydayCombo accountConfigs = runTestBlockState @P7 $ hour = Duration 3_600_000 startEpoch = 10 startTriggerTime = 1000 - chainParams = DummyData.dummyChainParameters @ChainParametersV2 + chainParams = DummyData.dummyChainParameters' @ChainParametersV2 cooldownDuration = chainParams ^. cpCooldownParameters . cpUnifiedCooldown -- | Test that missed rounds are carried over when rotating current capital distribution. diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/MaxLockDurationUpdate.hs b/concordium-consensus/tests/scheduler/SchedulerTests/MaxLockDurationUpdate.hs new file mode 100644 index 0000000000..1a5c2a003f --- /dev/null +++ b/concordium-consensus/tests/scheduler/SchedulerTests/MaxLockDurationUpdate.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +-- | Tests for max-lock-duration chain updates. +module SchedulerTests.MaxLockDurationUpdate (tests) where + +import qualified SchedulerTests.Helpers as Helpers +import Test.Hspec + +import qualified Concordium.GlobalState.DummyData as DummyData +import qualified Concordium.GlobalState.Persistent.Account as BS +import qualified Concordium.Scheduler.Runner as Runner +import Concordium.Scheduler.Types +import qualified Concordium.Types as Types + +-- | Test P11 scheduler handling of max-lock-duration chain updates. +tests :: Spec +tests = describe "MaxLockDurationUpdate" $ do + specify "P11 max lock duration chain updates are accepted and enqueued" $ do + Helpers.runSchedulerTestAssertIntermediateStates + @'Types.P11 + Helpers.defaultTestConfig + initialBlockState + [ Helpers.BlockItemAndAssertion + { biaaTransaction = maxLockDurationUpdate, + biaaAssertion = \result _ -> + return $ + Helpers.assertSuccessWithEvents + [UpdateEnqueued effectiveTime payload] + result + } + ] + where + initialBlockState = Helpers.createTestBlockStateWithAccounts @'Types.P11 ([] :: [BS.PersistentAccount (Types.AccountVersionFor 'Types.P11)]) + effectiveTime = 123456789 + timeout = 123456788 + payload = MaxLockDurationUpdatePayload (Duration 123456) + maxLockDurationUpdate = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = effectiveTime, + ctTimeout = timeout, + ctPayload = payload, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + } diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/MetaUpdateTransactions.hs b/concordium-consensus/tests/scheduler/SchedulerTests/MetaUpdateTransactions.hs new file mode 100644 index 0000000000..7691f6c3b5 --- /dev/null +++ b/concordium-consensus/tests/scheduler/SchedulerTests/MetaUpdateTransactions.hs @@ -0,0 +1,411 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +-- | Tests for meta-update transactions. +module SchedulerTests.MetaUpdateTransactions (tests) where + +import Control.Monad +import Data.Bool.Singletons +import qualified Data.Map as Map +import Data.Maybe +import qualified Data.Sequence as Seq +import Data.Word +import Test.HUnit +import Test.Hspec + +import qualified Concordium.Cost as Cost +import qualified Concordium.Crypto.SignatureScheme as SigScheme +import Concordium.ID.Types as ID +import qualified Concordium.Types.ProtocolLevelTokens.CBOR as CBOR +import Concordium.Types.Queries.Tokens +import Concordium.Types.Tokens + +import qualified Concordium.GlobalState.BlockState as BS +import qualified Concordium.GlobalState.DummyData as DummyData +import qualified Concordium.GlobalState.Persistent.Account as BS +import qualified Concordium.GlobalState.Persistent.BlobStore as Blob +import qualified Concordium.GlobalState.Persistent.BlockState as BS +import Concordium.Scheduler.DummyData +import Concordium.Scheduler.ProtocolLevelTokens.Module (tokenModuleV0Ref) +import Concordium.Scheduler.ProtocolLevelTokens.Queries +import qualified Concordium.Scheduler.Runner as Runner +import Concordium.Scheduler.Types +import qualified Concordium.Scheduler.Types as Types +import qualified Concordium.Types.DummyData as DummyData + +import qualified SchedulerTests.Helpers as Helpers + +dummyKP :: SigScheme.KeyPair +dummyKP = Helpers.keyPairFromSeed 1 + +-- | Address of 'dummyAccount'. +dummyAddress :: AccountAddress +dummyAddress = Helpers.accountAddressFromSeed 1 + +-- | Address of 'dummyAccount2'. +dummyAddress2 :: AccountAddress +dummyAddress2 = Helpers.accountAddressFromSeed 2 + +dummyAccount :: + (IsAccountVersion av, Blob.MonadBlobStore m) => + m (BS.PersistentAccount av) +dummyAccount = Helpers.makeTestAccountFromSeed 20_000_000 1 + +dummyAccount2 :: + (IsAccountVersion av, Blob.MonadBlobStore m) => + m (BS.PersistentAccount av) +dummyAccount2 = Helpers.makeTestAccountFromSeed 20_000_000 2 + +-- | Signing keys for 'dummyAccount'. +keys1 :: [(CredentialIndex, [(KeyIndex, SigScheme.KeyPair)])] +keys1 = [(0, [(0, dummyKP)])] + +-- | Create initial block state +initialBlockState :: + (IsProtocolVersion pv) => + Helpers.PersistentBSM pv (BS.HashedPersistentBlockState pv) +initialBlockState = + Helpers.createTestBlockStateWithAccountsM + [ dummyAccount, + dummyAccount2 + ] + +makeMetaTx :: + AccountAddress -> + Nonce -> + Energy -> + [(CredentialIndex, [(KeyIndex, SigScheme.KeyPair)])] -> + [CBOR.MetaUpdateOperation] -> + Runner.BlockItemDescription +makeMetaTx sendAddr nonce nrg keys ops = + Runner.AccountTx + Runner.TJSON + { payload = Runner.MetaUpdate{muOperations = mkOps ops}, + metadata = makeDummyHeader sendAddr nonce nrg, + keys = keys + } + where + mkOps = + Types.rawCborFromBytes + . CBOR.metaUpdateTransactionToBytes + . CBOR.MetaUpdateTransaction + . Seq.fromList + +-- | Test an empty meta-update transaction at a given protocol version. +-- The transaction should be accepted if and only if the protocol version supports meta-update +-- transactions. +testMetaUpdateSupport :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> Spec +testMetaUpdateSupport spv = it desc $ do + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + where + desc = + "Empty meta-update operation " + ++ (if supportsMetaUpdate spv then "" else "not ") + ++ "supported" + -- Base cost: payload size = 6 = 1 (type) + 4 (CBOR size) + 1 (CBOR encoding of empty list) + costFail = Cost.baseCost (transactionHeaderSize + 6) 1 + costSuccess = costFail + Cost.metaUpdateBaseCost + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion + { biaaTransaction = makeMetaTx dummyAddress 1 1000 keys1 [], + biaaAssertion = \result _newState -> do + return $ + if supportsMetaUpdate spv + then do + Helpers.assertSuccessWithEvents [] result + assertEqual "Used energy" costSuccess (Helpers.srUsedEnergy result) + else do + Helpers.assertRejectWithReason SerializationFailure result + assertEqual "Used energy" costFail (Helpers.srUsedEnergy result) + } + ] + +-- | Helper for creating a PLT with a 'Helpers.BlockItemAndAssertion'. +createPltBiaa :: TokenId -> Word8 -> CBOR.TokenInitializationParameters -> UpdateSequenceNumber -> Helpers.BlockItemAndAssertion pv +createPltBiaa pltName numDecimals initParam seqNum = + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = seqNum, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + ( [TokenCreated{etcPayload = createPLT}] + <> [ TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount dummyAddress, + etmAmount = mintAmt + } + | Just mintAmt <- [CBOR.tipInitialSupply initParam] + ] + ) + result + } + where + createPLT = Types.CreatePLT pltName tokenModuleV0Ref numDecimals tp + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes initParam + +-- | Create a "pltX" token. +createPlt1 :: UpdateSequenceNumber -> Helpers.BlockItemAndAssertion pv +createPlt1 = + createPltBiaa (TokenId "pltX") 2 $ + CBOR.TokenInitializationParameters + { tipName = Just "Test PLT 1", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://pltX.token", + tipGovernanceAccount = Just $ CBOR.accountTokenHolder dummyAddress, + tipAllowList = Nothing, + tipDenyList = Nothing, + tipInitialSupply = Just (TokenAmount 10000 2), + tipMintable = Just True, + tipBurnable = Just True, + tipAdditional = Map.empty + } + +-- | Create a "pltY" token. +createPlt2 :: UpdateSequenceNumber -> Helpers.BlockItemAndAssertion pv +createPlt2 = + createPltBiaa (TokenId "pltY") 0 $ + CBOR.TokenInitializationParameters + { tipName = Just "Test PLT 2", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://pltY.token", + tipGovernanceAccount = Just $ CBOR.accountTokenHolder dummyAddress, + tipAllowList = Just True, + tipDenyList = Just True, + tipInitialSupply = Nothing, + tipMintable = Just True, + tipBurnable = Just True, + tipAdditional = Map.empty + } + +-- | An alias for an 'AccountAddress' that is distinct. +distinctAlias :: AccountAddress -> AccountAddress +distinctAlias addr + | alias == addr = alias2 + | otherwise = alias + where + alias = createAlias addr 0 + alias2 = createAlias addr 1 + +-- | A collection of 'CBOR.MetaUpdateOperations' for testing. +metaUpdateMultiOperation :: [CBOR.MetaUpdateOperation] +metaUpdateMultiOperation = + [ CBOR.MetaTokenUpdate (TokenId "pltX") $ + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 100 2, + ttRecipient = CBOR.accountTokenHolder dummyAddress2, + ttMemo = Nothing + }, + CBOR.MetaTokenUpdate (TokenId "pltY") $ + CBOR.TokenMint $ + TokenAmount 100000 0, + CBOR.MetaTokenUpdate (TokenId "pltX") $ + CBOR.TokenPause, + CBOR.MetaTokenUpdate (TokenId "pltY") $ + CBOR.TokenAddAllowList (CBOR.accountTokenHolderShort dummyAddress2), + CBOR.MetaTokenUpdate (TokenId "pltY") $ + CBOR.TokenAddDenyList (CBOR.accountTokenHolder (distinctAlias dummyAddress)), + CBOR.MetaTokenUpdate (TokenId "pltY") $ + CBOR.TokenAddAllowList (CBOR.accountTokenHolder dummyAddress), + CBOR.MetaTokenUpdate (TokenId "PLTY") $ + CBOR.TokenRemoveDenyList (CBOR.accountTokenHolder dummyAddress), + CBOR.MetaTokenUpdate (TokenId "pltY") $ + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 2200 0, + ttRecipient = CBOR.accountTokenHolder dummyAddress2, + ttMemo = Just $ CBOR.CBORMemo (Memo "\xa0") + }, + CBOR.MetaTokenUpdate (TokenId "pltX") $ + CBOR.TokenUnpause, + CBOR.MetaTokenUpdate (TokenId "PltX") $ + CBOR.TokenBurn $ + TokenAmount 10 2, + CBOR.MetaTokenUpdate (TokenId "plty") $ + CBOR.TokenRemoveAllowList (CBOR.accountTokenHolderShort dummyAddress) + ] + +-- | The expected events from executing 'metaUpdateMultiOperations'. +metaUpdateMultiEvents :: [Event] +metaUpdateMultiEvents = + [ TokenTransfer + { ettTokenId = pltX, + ettFrom = holder1, + ettTo = holder2, + ettAmount = TokenAmount{taValue = 100, taDecimals = 2}, + ettMemo = Nothing, + ettFromLock = Nothing, + ettToLock = Nothing + }, + TokenMint + { etmTokenId = pltY, + etmTarget = holder1, + etmAmount = TokenAmount{taValue = 100000, taDecimals = 0} + }, + TokenModuleEvent + { etmeTokenId = pltX, + etmeType = TokenEventType "pause", + etmeDetails = CBOR.emptyEventDetails + }, + TokenModuleEvent + { etmeTokenId = pltY, + etmeType = TokenEventType "addAllowList", + etmeDetails = CBOR.encodeTargetDetails (CBOR.accountTokenHolderShort dummyAddress2) + }, + TokenModuleEvent + { etmeTokenId = pltY, + etmeType = TokenEventType "addDenyList", + etmeDetails = CBOR.encodeTargetDetails (CBOR.accountTokenHolder (distinctAlias dummyAddress)) + }, + TokenModuleEvent + { etmeTokenId = pltY, + etmeType = TokenEventType "addAllowList", + etmeDetails = CBOR.encodeTargetDetails (CBOR.accountTokenHolder dummyAddress) + }, + TokenModuleEvent + { etmeTokenId = pltY, + etmeType = TokenEventType "removeDenyList", + etmeDetails = CBOR.encodeTargetDetails (CBOR.accountTokenHolder dummyAddress) + }, + TokenTransfer + { ettTokenId = pltY, + ettFrom = holder1, + ettTo = holder2, + ettAmount = TokenAmount{taValue = 2200, taDecimals = 0}, + ettMemo = Just (Memo "\xa0"), + ettFromLock = Nothing, + ettToLock = Nothing + }, + TokenModuleEvent + { etmeTokenId = pltX, + etmeType = TokenEventType "unpause", + etmeDetails = CBOR.emptyEventDetails + }, + TokenBurn + { etbTokenId = pltX, + etbTarget = holder1, + etbAmount = TokenAmount{taValue = 10, taDecimals = 2} + }, + TokenModuleEvent + { etmeTokenId = pltY, + etmeType = TokenEventType "removeAllowList", + etmeDetails = CBOR.encodeTargetDetails (CBOR.accountTokenHolderShort dummyAddress) + } + ] + where + pltX = TokenId "pltX" + pltY = TokenId "pltY" + holder1 = HolderAccount dummyAddress + holder2 = HolderAccount dummyAddress2 + +-- | Test a meta-update transaction that consists of multiple steps and involves multiple PLTs. +testMetaUpdateMulti :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> Spec +testMetaUpdateMulti spv = case sSupportsPLT (sAccountVersionFor spv) of + SFalse -> return () + STrue -> + when (supportsMetaUpdate spv) $ + it "Multi-token multi-step meta-update" $ + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + where + transactionsAndAssertions :: (PVSupportsPLT pv) => [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ createPlt1 1, + createPlt2 2, + Helpers.BlockItemAndAssertion + { biaaTransaction = + makeMetaTx dummyAddress 1 10000 keys1 metaUpdateMultiOperation, + biaaAssertion = \result newST -> do + st <- BS.freezeBlockState newST + tiX <- queryTokenInfo (TokenId "pltX") st + tiY <- queryTokenInfo (TokenId "pltY") st + acc1 <- fromJust <$> BS.getAccount st dummyAddress + ai1 <- queryAccountTokens acc1 st + acc2 <- fromJust <$> BS.getAccount st dummyAddress2 + ai2 <- queryAccountTokens acc2 st + return $ do + Helpers.assertSuccessWithEvents metaUpdateMultiEvents result + assertEqual "used energy" 1803 (Helpers.srUsedEnergy result) + assertEqual + "pltX supply" + (Right $ TokenAmount 9990 2) + (tsTotalSupply . tiTokenState <$> tiX) + assertEqual + "pltY supply" + (Right $ TokenAmount 100000 0) + (tsTotalSupply . tiTokenState <$> tiY) + assertEqual + "account 1 tokens" + [ Token + (TokenId "pltX") + ( TokenAccountState + { moduleAccountState = Just "\xa0", + balance = TokenAmount 9890 2 + } + ), + Token + (TokenId "pltY") + ( TokenAccountState + { moduleAccountState = + Just + "\xa2\x68\ + \denyList\xf4\x69\ + \allowList\xf4", + balance = + TokenAmount 97800 0 + } + ) + ] + ai1 + assertEqual + "account 2 tokens" + [ Token + (TokenId "pltX") + ( TokenAccountState + { moduleAccountState = Just "\xa0", + balance = TokenAmount 100 2 + } + ), + Token + (TokenId "pltY") + ( TokenAccountState + { moduleAccountState = + Just + "\xa2\x68\ + \denyList\xf4\x69\ + \allowList\xf5", + balance = TokenAmount 2200 0 + } + ) + ] + ai2 + } + ] + +-- | Scheduler tests for meta-update transactions. +tests :: Spec +tests = parallel $ + describe "Meta-update transactions" $ + sequence_ $ + Helpers.forEveryProtocolVersion $ \spv pvString -> do + describe pvString $ do + testMetaUpdateSupport spv + testMetaUpdateMulti spv diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs b/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs index ee8000b5ab..e65d23a3c8 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/Payday.hs @@ -108,7 +108,7 @@ testDoMintingP4 = do DummyData.dummyIdentityProviders DummyData.dummyArs DummyData.dummyKeyCollection - DummyData.dummyChainParameters + (DummyData.dummyChainParameters @'P4) -- Run a test of doMintingP4. It is provided a list of updates (paired with effective slot time), -- a list of expected special transaction outcomes to have been produced, the expected balance -- on the foundation account and the expected bank status. @@ -123,7 +123,7 @@ testDoMintingP4 = do initialState <- thawBlockState =<< initialBlockState newState <- doMintingP4 - dummyChainParameters + (dummyChainParameters @'P4) targetEpoch mintRate foundationAccount @@ -305,7 +305,7 @@ genesis nBakers = [] 1_234 (withIsAuthorizationsVersionFor (protocolVersion @pv) dummyKeyCollection) - dummyChainParameters + (dummyChainParameters @pv) type MyPersistentTreeState pv = SkovPersistentData pv type MyPersistentMonad pv = diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/RustScheduler.hs b/concordium-consensus/tests/scheduler/SchedulerTests/RustScheduler.hs new file mode 100644 index 0000000000..2544008958 --- /dev/null +++ b/concordium-consensus/tests/scheduler/SchedulerTests/RustScheduler.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module SchedulerTests.RustScheduler where + +import qualified Concordium.GlobalState.BlockState as BlockState +import qualified Concordium.Scheduler.Environment as EI +import qualified Concordium.Scheduler.ProtocolLevelTokens.RustPLTScheduler as RustPLTScheduler +import qualified Concordium.Types as Types +import qualified Concordium.Types.Execution as Execution +import qualified Control.Exception as Exception +import qualified Control.Monad.Catch as Catch +import qualified Data.ByteString.Short as ByteString +import qualified SchedulerTests.Helpers as Helpers +import Test.HUnit +import Test.Hspec + +testExecuteTransaction :: Spec +testExecuteTransaction = describe "executeTransaction" $ do + it "throws when called with a non-supported payload type" $ do + assertions <- Helpers.runTestBlockState @'Types.P11 $ do + account0 <- Helpers.makeTestAccountFromSeed 2_000_000 0 + blockStateBefore <- Helpers.createTestBlockStateWithAccounts [account0] + blockStateBeforeThawed <- BlockState.thawBlockState blockStateBefore + let schedulerState = EI.makeInitialSchedulerState @(Helpers.PersistentBSM 'Types.P11) blockStateBeforeThawed + let depositContext :: EI.WithDepositContext (Helpers.PersistentBSM 'Types.P11) = + EI.WithDepositContext + { _wtcSenderAccount = (0, account0), + _wtcPayerAccount = (0, account0), + _wtcTransactionType = Execution.TTRegisterData, + _wtcTransactionHash = read "0000000000000000000000000000000000000000000000000000000000000000", + _wtcSenderAddress = Helpers.accountAddressFromSeed 0, + _wtcSponsorAddress = Nothing, + _wtcEnergyAmount = 500_000, + _wtcTransactionCheckHeaderCost = 1_000, + _wtcCurrentlyUsedBlockEnergy = 0, + _wtcTransactionIndex = 0, + _wtcTransactionSequenceNumber = 1 + } + let schedulerComputation = + RustPLTScheduler.executeTransaction + @(Helpers.PersistentBSM 'Types.P11) + depositContext + (Execution.RegisterData $ Types.RegisteredData ByteString.empty) + let contextState = + EI.ContextState + { _chainMetadata = + Types.ChainMetadata + { slotTime = 500 + }, + _maxBlockEnergy = 500_000_000, + _accountCreationLimit = 500 + } + errorMessage <- + Catch.catch + @_ + @Exception.ErrorCall + (EI.runSchedulerT schedulerComputation contextState schedulerState >> return Nothing) + (\message -> return $ Just message) + return $ do + case errorMessage of + Nothing -> assertFailure "Expected an exception to be thrown" + Just (Exception.ErrorCall message) -> assertEqual "Expected error message" "Call to 'ffiExecuteTransaction' resulted in panic with message: \"Unexpected failure during transaction execution: UnexpectedPayload\"" message + assertions + +tests :: Spec +tests = describe "RustScheduler" $ do + testExecuteTransaction diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/SmartContracts/V1/Queries.hs b/concordium-consensus/tests/scheduler/SchedulerTests/SmartContracts/V1/Queries.hs index 2160f5aff7..9b284ad267 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/SmartContracts/V1/Queries.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/SmartContracts/V1/Queries.hs @@ -78,7 +78,7 @@ keyPair1 :: SigScheme.KeyPair keyPair1 = Helpers.keyPairFromSeed 1 blockEnergyRate :: Types.EnergyRate -blockEnergyRate = dummyChainParameters @'Types.ChainParametersV1 ^. Types.energyRate +blockEnergyRate = dummyChainParameters' @'Types.ChainParametersV1 ^. Types.energyRate accountBalanceSourceFile :: FilePath accountBalanceSourceFile = "../concordium-base/smart-contracts/testdata/contracts/v1/queries-account-balance.wasm" @@ -749,8 +749,8 @@ exchangeRatesTestCase spv pvString = Wasm.putExchangeRateLE currentEuroPerEnergy Wasm.putExchangeRateLE currentAmountPerEnergy - currentEuroPerEnergy = dummyChainParameters @'Types.ChainParametersV1 ^. Types.euroPerEnergy - currentAmountPerEnergy = dummyChainParameters @'Types.ChainParametersV1 ^. Types.microGTUPerEuro + currentEuroPerEnergy = dummyChainParameters' @'Types.ChainParametersV1 ^. Types.euroPerEnergy + currentAmountPerEnergy = dummyChainParameters' @'Types.ChainParametersV1 ^. Types.microGTUPerEuro allSourceFile :: FilePath allSourceFile = "../concordium-base/smart-contracts/testdata/contracts/v1/queries-all.wasm" diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TokenCreation.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TokenCreation.hs index e0aa45a9a3..9662d71d74 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TokenCreation.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TokenCreation.hs @@ -10,11 +10,11 @@ module SchedulerTests.TokenCreation (tests) where import qualified Codec.CBOR.Term as CBOR import Data.Bool.Singletons -import qualified Data.ByteString.Short as BSS import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Vector as Vec import qualified SchedulerTests.Helpers as Helpers +import Test.HUnit import Test.Hspec import qualified Concordium.Crypto.DummyData as DummyData @@ -22,13 +22,15 @@ import qualified Concordium.Crypto.SHA256 as Hash import Concordium.ID.Types as ID import qualified Concordium.Types.DummyData as DummyData import qualified Concordium.Types.ProtocolLevelTokens.CBOR as CBOR -import Concordium.Types.Tokens +import Concordium.Types.Queries.Tokens import Concordium.Types.Updates +import qualified Concordium.GlobalState.BlockState as BS import qualified Concordium.GlobalState.DummyData as DummyData import qualified Concordium.GlobalState.Persistent.Account as BS import qualified Concordium.GlobalState.Persistent.BlobStore as Blob import qualified Concordium.GlobalState.Persistent.BlockState as BS +import Concordium.Scheduler.ProtocolLevelTokens.Queries import qualified Concordium.Scheduler.Runner as Runner import Concordium.Scheduler.Types import qualified Concordium.Scheduler.Types as Types @@ -44,11 +46,10 @@ dummyTokenHolder :: TokenHolder dummyTokenHolder = HolderAccount dummyAddress2 dummyCborAccountAddress :: CBOR.CborAccountAddress -dummyCborAccountAddress = - CBOR.CborAccountAddress - { chaAccount = dummyAddress2, - chaCoinInfo = Nothing - } +dummyCborAccountAddress = CBOR.accountTokenHolder dummyAddress2 + +dummyCborAccountAddressShort :: CBOR.CborAccountAddress +dummyCborAccountAddressShort = CBOR.accountTokenHolderShort dummyAddress2 dummyAccount :: (IsAccountVersion av, Blob.MonadBlobStore m) => @@ -98,13 +99,31 @@ initialBlockStateWithCustomKeys numKeys authorizedKeys threshold = do testCreatePLT :: forall pv. (IsProtocolVersion pv, PVSupportsPLT pv) => SProtocolVersion pv -> String -> Spec testCreatePLT _ pvString = describe pvString $ do + -- Test without initial supply it "Create PLT - no initial supply" $ do let transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1, - biaaAssertion = \result _ -> do - return $ Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt1 st + return $ do + Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + assertEqual + "PLT list" + [plt1] + pltList + assertEqual + "Token info" + ( Right $ + expectedTokenInfo + plt1 + (expectModuleState params1) + (TokenAmount 0 0) + ) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -112,18 +131,35 @@ testCreatePLT _ pvString = describe pvString $ do Helpers.defaultTestConfig initialBlockState transactionsAndAssertions - it "Create PLT - initial supply" $ do + -- Test with initial supply, and using governance account specififed wihtout coin info + it "Create PLT - initial supply - no coin info" $ do let transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT2, - biaaAssertion = \result _ -> do - return $ + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt2 st + return $ do Helpers.assertSuccessWithEvents [ TokenCreated{etcPayload = createPLT2}, TokenMint{etmTokenId = plt2, etmAmount = TokenAmount 10 0, etmTarget = dummyTokenHolder} ] result + assertEqual + "PLT list" + [plt2] + pltList + assertEqual + "Token info" + ( Right $ + expectedTokenInfo + plt2 + (expectModuleState params2) + (TokenAmount 10 0) + ) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -131,6 +167,7 @@ testCreatePLT _ pvString = describe pvString $ do Helpers.defaultTestConfig initialBlockState transactionsAndAssertions + -- Tests initialization with as few parameters specified as possible it "Create PLT - minimal parameters" $ do let createPLT1MinimalParameters = Types.CreatePLT @@ -152,8 +189,32 @@ testCreatePLT _ pvString = describe pvString $ do transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1MinimalParameters, - biaaAssertion = \result _ -> do - return $ Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1MinimalParameters}] result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt1 st + return $ do + Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1MinimalParameters}] result + assertEqual + "PLT list" + [plt1] + pltList + assertEqual + "Token info" + ( Right $ + expectedTokenInfo + plt1 + ( expectModuleState + params1 + { CBOR.tipMintable = Just False, + CBOR.tipBurnable = Just False, + CBOR.tipDenyList = Just False, + CBOR.tipAllowList = Just False + } + ) + (TokenAmount 0 0) + ) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -174,8 +235,20 @@ testCreatePLT _ pvString = describe pvString $ do transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1MissingNameParameter, - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason (TokenInitializeFailure "Token initialization parameters could not be deserialized: Token name is missing") result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + tokenInfo <- queryTokenInfo plt1 st + pltList <- queryPLTList st + return $ do + Helpers.assertUpdateFailureWithReason (TokenInitializeFailure "Token initialization parameters could not be deserialized: Token name is missing") result + assertEqual + "PLT list" + [] + pltList + assertEqual + "Token info" + (Left QTMEUnknownToken) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -203,8 +276,25 @@ testCreatePLT _ pvString = describe pvString $ do transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1AdditionalNameParameter, - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason (TokenInitializeFailure "Token initialization parameters could not be deserialized: Unknown additional parameters: [\"_param1\"]") result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt1 st + return $ do + Helpers.assertUpdateFailureWhere + ( \case + TokenInitializeFailure _ -> return () + _ -> assertFailure "not TokenInitializeFailure" + ) + result + assertEqual + "PLT list" + [] + pltList + assertEqual + "Token info" + (Left QTMEUnknownToken) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -217,13 +307,42 @@ testCreatePLT _ pvString = describe pvString $ do transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1, - biaaAssertion = \result _ -> do - return $ Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt1 st + return $ do + Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + assertEqual + "PLT list" + [plt1] + pltList + assertEqual + "Token info" + ( Right $ + expectedTokenInfo + plt1 + (expectModuleState params1) + (TokenAmount 0 0) + ) + tokenInfo }, Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 2 createPLT1, - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason (DuplicateTokenId plt1) result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt2 st + return $ do + Helpers.assertUpdateFailureWithReason (DuplicateTokenId plt1) result + assertEqual + "PLT list" + [plt1] + pltList + assertEqual + "Token info" + (Left QTMEUnknownToken) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -243,8 +362,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(1, DummyData.deterministicKP 1), (2, DummyData.deterministicKP 2)], - biaaAssertion = \result _ -> do - return $ Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + assertEqual + "PLT list" + [plt1] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -264,8 +390,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(1, DummyData.deterministicKP 1), (2, DummyData.deterministicKP 2)], - biaaAssertion = \result _ -> do - return $ Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertSuccessWithEvents [TokenCreated{etcPayload = createPLT1}] result + assertEqual + "PLT list" + [plt1] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -285,8 +418,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(1, DummyData.deterministicKP 1), (2, DummyData.deterministicKP 23)], - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason IncorrectSignature result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertUpdateFailureWithReason IncorrectSignature result + assertEqual + "PLT list" + [] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -306,8 +446,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(0, DummyData.deterministicKP 0)], - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason IncorrectSignature result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertUpdateFailureWithReason IncorrectSignature result + assertEqual + "PLT list" + [] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -327,8 +474,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(1, DummyData.deterministicKP 1), (2, DummyData.deterministicKP 2)], - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason IncorrectSignature result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertUpdateFailureWithReason IncorrectSignature result + assertEqual + "PLT list" + [] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -348,8 +502,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(0, DummyData.deterministicKP 0)], - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason IncorrectSignature result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + assertEqual + "PLT list" + [] + pltList + Helpers.assertUpdateFailureWithReason IncorrectSignature result } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -369,8 +530,15 @@ testCreatePLT _ pvString = describe pvString $ do 1 createPLT1 [(0, DummyData.deterministicKP 1)], - biaaAssertion = \result _ -> do - return $ Helpers.assertUpdateFailureWithReason IncorrectSignature result + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + return $ do + Helpers.assertUpdateFailureWithReason IncorrectSignature result + assertEqual + "PLT list" + [] + pltList } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -384,11 +552,22 @@ testCreatePLT _ pvString = describe pvString $ do transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = txCreatePLT 1 createPLT1{_cpltTokenModule = invalidRef}, - biaaAssertion = \result _ -> do - return $ + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + pltList <- queryPLTList st + tokenInfo <- queryTokenInfo plt1 st + return $ do Helpers.assertUpdateFailureWithReason (InvalidTokenModuleRef invalidRef) result + assertEqual + "PLT list" + [] + pltList + assertEqual + "Token info" + (Left QTMEUnknownToken) + tokenInfo } ] Helpers.runSchedulerTestAssertIntermediateStates @@ -424,7 +603,7 @@ testCreatePLT _ pvString = describe pvString $ do tipBurnable = Just True, tipAdditional = Map.empty } - toTokenParam = Types.TokenParameter . BSS.toShort . CBOR.tokenInitializationParametersToBytes + toTokenParam = Types.rawCborFromBytes . CBOR.tokenInitializationParametersToBytes createPLT1 = Types.CreatePLT { _cpltTokenModule = testModuleRef, @@ -436,9 +615,11 @@ testCreatePLT _ pvString = describe pvString $ do CBOR.TokenInitializationParameters { tipName = Just "Protocol-level token", tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", - tipGovernanceAccount = Just dummyCborAccountAddress, + -- Uses cbor account address without coin info + tipGovernanceAccount = Just dummyCborAccountAddressShort, tipAllowList = Just False, tipDenyList = Just False, + -- Has initial supply tipInitialSupply = Just (TokenAmount 10 0), tipMintable = Just True, tipBurnable = Just True, @@ -451,6 +632,29 @@ testCreatePLT _ pvString = describe pvString $ do _cpltInitializationParameters = toTokenParam params2, _cpltDecimals = 0 } + expectedTokenInfo tiTokenId tsModuleState tsTotalSupply = + TokenInfo + { tiTokenState = + TokenState + { tsTokenModuleRef = _cpltTokenModule createPLT2, + tsDecimals = _cpltDecimals createPLT2, + .. + }, + .. + } + expectModuleState params = + CBOR.tokenModuleStateToBytes $ + CBOR.TokenModuleState + { tmsName = CBOR.tipName params, + tmsMetadata = CBOR.tipMetadata params, + tmsGovernanceAccount = CBOR.accountTokenHolder <$> CBOR.chaAccount <$> CBOR.tipGovernanceAccount params, + tmsPaused = Just False, + tmsAllowList = CBOR.tipAllowList params, + tmsDenyList = CBOR.tipDenyList params, + tmsMintable = CBOR.tipMintable params, + tmsBurnable = CBOR.tipBurnable params, + tmsAdditional = mempty + } tests :: Spec tests = diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TokenHolderTransactions.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TokenHolderTransactions.hs index a38babe5ce..7b82953d41 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TokenHolderTransactions.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TokenHolderTransactions.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -11,24 +12,35 @@ module SchedulerTests.TokenHolderTransactions (tests) where import qualified Concordium.Crypto.SignatureScheme as SigScheme import Concordium.ID.Types as ID import qualified Concordium.Types.ProtocolLevelTokens.CBOR as CBOR +import Concordium.Types.Queries.Tokens +import Concordium.Types.Tokens +import qualified Concordium.GlobalState.BlockState as BS import qualified Concordium.GlobalState.DummyData as DummyData import qualified Concordium.GlobalState.Persistent.Account as BS import qualified Concordium.GlobalState.Persistent.BlobStore as Blob import qualified Concordium.GlobalState.Persistent.BlockState as BS +import Concordium.GlobalState.Types (BlockState) import Concordium.Scheduler.DummyData import Concordium.Scheduler.ProtocolLevelTokens.Module (tokenModuleV0Ref) +import Concordium.Scheduler.ProtocolLevelTokens.Queries import qualified Concordium.Scheduler.Runner as Runner import Concordium.Scheduler.Types import qualified Concordium.Scheduler.Types as Types import qualified Concordium.Types.DummyData as DummyData import Data.Bool.Singletons +import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Short as BSS import qualified Data.Map as Map +import Data.Maybe +import qualified Data.Sequence as Seq import Data.String +import Data.Text (Text) import qualified SchedulerTests.Helpers as Helpers +import Test.HUnit import Test.Hspec +import Test.QuickCheck dummyKP :: SigScheme.KeyPair dummyKP = Helpers.keyPairFromSeed 1 @@ -39,13 +51,6 @@ dummyAddress = Helpers.accountAddressFromSeed 1 dummyAddress2 :: AccountAddress dummyAddress2 = Helpers.accountAddressFromSeed 2 -dummyCborAccountAddress :: CBOR.CborAccountAddress -dummyCborAccountAddress = - CBOR.CborAccountAddress - { chaAccount = dummyAddress2, - chaCoinInfo = Nothing - } - dummyAccount :: (IsAccountVersion av, Blob.MonadBlobStore m) => m (BS.PersistentAccount av) @@ -66,19 +71,17 @@ initialBlockState = dummyAccount2 ] --- | Test the following sequence of operations: --- - Attempt a token holder transaction for a non-existent token (TokenId: GTU). (Fails: non-existent token) --- - Create a token with the TokenId GTU. (Succeeds) --- - Attempt a token holder transaction for GTU. (Fails: CBOR deserialization) -testTokenHolder :: +-- | Test attempt a token holder transaction for a non-existent token (TokenId: GTU). (Fails: non-existent token) +testNonExistingToken :: forall pv. (IsProtocolVersion pv, PVSupportsPLT pv) => SProtocolVersion pv -> String -> Spec -testTokenHolder _ pvString = - specify (pvString ++ ": Token holder operations") $ do - let transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] +testNonExistingToken _ pvString = + specify (pvString ++ ": Non-existing token") $ do + let gtu = Types.TokenId $ fromString "Gtu" + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] transactionsAndAssertions = [ Helpers.BlockItemAndAssertion { biaaTransaction = @@ -87,22 +90,60 @@ testTokenHolder _ pvString = { payload = Runner.TokenUpdate { tuTokenId = gtu, - tuOperations = Types.TokenParameter BSS.empty + tuOperations = Types.rawCborFromBytes "" }, metadata = makeDummyHeader dummyAddress 1 1_000, keys = [(0, [(0, dummyKP)])] }, biaaAssertion = \result _ -> do return $ Helpers.assertRejectWithReason (NonExistentTokenId gtu) result - }, - Helpers.BlockItemAndAssertion + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + +-- | Test CBOR deserialization failure of token update operations. +testDeserializationFailure :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + String -> + Spec +testDeserializationFailure _ pvString = + specify (pvString ++ ": CBOR deserialization failure") $ do + let govAcct = CBOR.accountTokenHolder dummyAddress + gtu = Types.TokenId $ fromString "Gtu" + gtu2 = Types.TokenId $ fromString "gtU" + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just True, + tipDenyList = Just False, + tipInitialSupply = Nothing, + tipMintable = Just True, + tipBurnable = Just True, + tipAdditional = Map.empty + } + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + createPLT = Types.CreatePLT gtu tokenModuleV0Ref 0 tp + createPLTPayload = Types.CreatePLTUpdatePayload createPLT + gtuEvent = TokenCreated{etcPayload = createPLT} + + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion { biaaTransaction = Runner.ChainUpdateTx $ Runner.ChainUpdateTransaction { ctSeqNumber = 1, ctEffectiveTime = 0, ctTimeout = DummyData.dummyMaxTransactionExpiryTime, - ctPayload = plt, + ctPayload = createPLTPayload, ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] }, biaaAssertion = \result _ -> do @@ -115,16 +156,25 @@ testTokenHolder _ pvString = { payload = Runner.TokenUpdate { tuTokenId = gtu2, - tuOperations = Types.TokenParameter BSS.empty + tuOperations = Types.rawCborFromBytes "" }, - metadata = makeDummyHeader dummyAddress 2 1_000, + metadata = makeDummyHeader dummyAddress 1 1_000, keys = [(0, [(0, dummyKP)])] }, biaaAssertion = \result _ -> do return $ - Helpers.assertRejectWithReason - ( TokenUpdateTransactionFailed - (TokenModuleRejectReason{tmrrTokenId = gtu, tmrrType = errType, tmrrDetails = Just cborFail}) + Helpers.assertRejectWhere + ( \case + TokenUpdateTransactionFailed tmrj -> do + assertEqual + "Module reject reason token id" + (tmrrTokenId tmrj) + gtu + assertEqual + "Module reject reason type" + (tmrrType tmrj) + (Types.TokenEventType $ fromString "deserializationFailure") + _ -> assertFailure "not TokenUpdateTransactionFailed" ) result } @@ -134,37 +184,1552 @@ testTokenHolder _ pvString = Helpers.defaultTestConfig initialBlockState transactionsAndAssertions + +-- | Test two operations in a single transaction. +testTwoOperations :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + String -> + Spec +testTwoOperations _ pvString = + specify (pvString ++ ": Two operations in a transaction") $ do + let + govAcct = CBOR.accountTokenHolder dummyAddress + recptAcct = CBOR.accountTokenHolder dummyAddress2 + gtu = Types.TokenId $ fromString "Gtu" + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just False, + tipDenyList = Just False, + tipInitialSupply = Just $ TokenAmount 150 0, + tipMintable = Just False, + tipBurnable = Just False, + tipAdditional = Map.empty + } + paramsEncoded = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + createPLT = Types.CreatePLT gtu tokenModuleV0Ref 0 paramsEncoded + createPLTPayload = Types.CreatePLTUpdatePayload createPLT + testOps = + mkOps $ + CBOR.TokenUpdateTransaction $ + Seq.fromList + [ CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 10 0, + ttRecipient = recptAcct, + ttMemo = Nothing + }, + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 90 0, + ttRecipient = recptAcct, + ttMemo = Nothing + } + ] + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes + + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = createPLTPayload, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = gtu, + etmTarget = HolderAccount dummyAddress, + etmAmount = TokenAmount 150 0 + } + ] + result + }, + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = gtu, + tuOperations = testOps + }, + metadata = makeDummyHeader dummyAddress 1 1_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + senderIndex <- fromJust <$> BS.getAccount st dummyAddress + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + senderTokens <- queryAccountTokens senderIndex st + destTokens <- queryAccountTokens destIndex st + return $ do + assertEqual + "Sender tokens after rollback" + senderTokens + [ Token + { tokenAccountState = + TokenAccountState + { moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Nothing, + tmasAllowList = Nothing, + tmasAdditional = mempty + }, + balance = TokenAmount 50 0 + }, + tokenId = gtu + } + ] + assertEqual + "Recipient tokens after rollback" + destTokens + [ Token + { tokenAccountState = + TokenAccountState + { moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Nothing, + tmasAllowList = Nothing, + tmasAdditional = mempty + }, + balance = TokenAmount 100 0 + }, + tokenId = gtu + } + ] + Helpers.assertSuccessWithEvents + [ TokenTransfer + { ettTokenId = gtu, + ettFrom = HolderAccount $ CBOR.chaAccount govAcct, + ettTo = HolderAccount $ CBOR.chaAccount recptAcct, + ettAmount = TokenAmount 10 0, + ettMemo = Nothing, + ettFromLock = Nothing, + ettToLock = Nothing + }, + TokenTransfer + { ettTokenId = gtu, + ettFrom = HolderAccount $ CBOR.chaAccount govAcct, + ettTo = HolderAccount $ CBOR.chaAccount recptAcct, + ettAmount = TokenAmount 90 0, + ettMemo = Nothing, + ettFromLock = Nothing, + ettToLock = Nothing + } + ] + result + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + +-- | Test that if an operation in a transaction fails, then preceding operations are +-- rolled back. +testRollback :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + String -> + Spec +testRollback _ pvString = + specify (pvString ++ ": State rollback") $ do + let mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes + govAcct = CBOR.accountTokenHolder dummyAddress + recptAcct = CBOR.accountTokenHolder dummyAddress2 + gtu = Types.TokenId $ fromString "Gtu" + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just False, + tipDenyList = Just False, + tipInitialSupply = Just $ TokenAmount 150 0, + tipMintable = Just False, + tipBurnable = Just False, + tipAdditional = Map.empty + } + paramsEncoded = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + createPLT = Types.CreatePLT gtu tokenModuleV0Ref 0 paramsEncoded + createPLTPayload = Types.CreatePLTUpdatePayload createPLT + testOps = + mkOps $ + CBOR.TokenUpdateTransaction $ + Seq.fromList + [ CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 100 0, + ttRecipient = recptAcct, + ttMemo = Nothing + }, + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 200 0, + ttRecipient = recptAcct, + ttMemo = Nothing + } + ] + assertTokenReject trr = + Helpers.assertRejectWithReason + . TokenUpdateTransactionFailed + . makeTokenModuleRejectReason gtu + . CBOR.encodeTokenRejectReason + $ trr + + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = createPLTPayload, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = gtu, + etmTarget = HolderAccount dummyAddress, + etmAmount = TokenAmount 150 0 + } + ] + result + }, + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = gtu, + tuOperations = testOps + }, + metadata = makeDummyHeader dummyAddress 1 1_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + senderIndex <- fromJust <$> BS.getAccount st dummyAddress + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + senderTokens <- queryAccountTokens senderIndex st + destTokens <- queryAccountTokens destIndex st + return $ do + assertEqual + "Sender tokens after rollback" + senderTokens + [ Token + { tokenAccountState = + TokenAccountState + { moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Nothing, + tmasAllowList = Nothing, + tmasAdditional = mempty + }, + balance = TokenAmount 150 0 + }, + tokenId = gtu + } + ] + assertEqual + "Recipient tokens after rollback" + destTokens + [] + assertTokenReject + CBOR.TokenBalanceInsufficient + { trrOperationIndex = 1, + trrRequiredBalance = TokenAmount 200 0, + trrAvailableBalance = TokenAmount 50 0 + } + result + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + +-- | A configuration for testing token holder transactions. This specifies testing conditions to +-- use for a transfer. +data TransferConfig = TransferConfig + { -- | Is the token paused? + tcPaused :: Bool, + -- | Does the sender have sufficient balance? + tcSenderBalanceSufficient :: Bool, + -- | Allow list configured? + tcAllowList :: Bool, + -- | Sender on allow list? + tcSenderAllow :: Bool, + -- | Recipient on allow list? + tcRecvAllow :: Bool, + -- | Deny list configured? + tcDenyList :: Bool, + -- | Sender on deny list? + tcSenderDeny :: Bool, + -- | Recipient on deny list? + tcRecvDeny :: Bool, + -- | Recipient invalid? + tcRecvInvalid :: Bool, + -- | Energy insufficient? + tcEnergyInsufficient :: Bool, + -- | Transfer memo + tcMemo :: Maybe CBOR.TaggableMemo, + -- | Use short form of recipient address? + tcShortRecv :: Bool, + -- | Use an alias for the sender account? + tcSenderAlias :: Bool, + -- | Use an alias for the recipient account? + tcRecvAlias :: Bool + } + deriving (Eq, Show) + +instance Arbitrary TransferConfig where + arbitrary = do + tcPaused <- arbitrary + tcSenderBalanceSufficient <- arbitrary + tcEnergyInsufficient <- arbitrary + tcRecvInvalid <- arbitrary + tcAllowList <- arbitrary + tcDenyList <- arbitrary + tcSenderAllow <- (tcAllowList &&) <$> arbitrary + tcRecvAllow <- ((tcAllowList && not tcRecvInvalid) &&) <$> arbitrary + tcSenderDeny <- (tcDenyList &&) <$> arbitrary + tcRecvDeny <- ((tcDenyList && not tcRecvInvalid) &&) <$> arbitrary + tcMemo <- + oneof + [ pure Nothing, + Just . CBOR.UntaggedMemo <$> genMemo, + Just . CBOR.CBORMemo <$> genMemo + ] + tcShortRecv <- arbitrary + tcSenderAlias <- arbitrary + tcRecvAlias <- arbitrary + return TransferConfig{..} + where + genMemo = do + len <- chooseBoundedIntegral (0, maxMemoSize) + Memo . BSS.pack <$> vector len + shrink tc = + [tc{tcAllowList = False, tcSenderAllow = False, tcRecvAllow = False} | tcAllowList tc] + ++ [tc{tcDenyList = False, tcSenderDeny = False, tcRecvDeny = False} | tcDenyList tc] + ++ [tc{tcMemo = Nothing} | isJust (tcMemo tc)] + +-- | An alias for an 'AccountAddress' that is distinct. +distinctAlias :: AccountAddress -> AccountAddress +distinctAlias addr + | alias == addr = alias2 + | otherwise = alias where - gtu = Types.TokenId $ fromString "Gtu" - gtu2 = Types.TokenId $ fromString "gtU" + alias = createAlias addr 0 + alias2 = createAlias addr 1 + +-- | This test constructs a PLT and then attempts a transfer. The setup for the transfer is +-- arbitrarily determined to cover possible failure cases and outside factors. +testTransfer :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + Property +testTransfer spv = property (ioProperty . theTest) + where + theTest TransferConfig{..} = do + let govAcct = CBOR.accountTokenHolder dummyAddress + recptAcct = CBOR.accountTokenHolder dummyAddress2 + mintAmt = TokenAmount 100 0 + excessiveAmt = TokenAmount 200 0 + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just tcAllowList, + tipDenyList = Just tcDenyList, + tipInitialSupply = Just mintAmt, + tipMintable = Nothing, + tipBurnable = Nothing, + tipAdditional = Map.empty + } + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + pltName = Types.TokenId $ fromString "PLT" + createPLT = Types.CreatePLT pltName tokenModuleV0Ref 0 tp + condOp True = Seq.singleton + condOp False = mempty + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes + initOps = + mkOps . CBOR.TokenUpdateTransaction $ + condOp tcSenderAllow (CBOR.TokenAddAllowList govAcct) + <> condOp tcRecvAllow (CBOR.TokenAddAllowList recptAcct) + <> condOp tcSenderDeny (CBOR.TokenAddDenyList govAcct) + <> condOp tcRecvDeny (CBOR.TokenAddDenyList recptAcct) + <> condOp tcPaused CBOR.TokenPause + invalidAddress = Helpers.accountAddressFromSeed (-1) + actualRecipientAddress + | tcRecvInvalid = invalidAddress + | tcRecvAlias = distinctAlias dummyAddress2 + | otherwise = dummyAddress2 + actualRecipient + | tcShortRecv = CBOR.accountTokenHolderShort actualRecipientAddress + | otherwise = CBOR.accountTokenHolder actualRecipientAddress + actualSenderAddress + | tcSenderAlias = distinctAlias dummyAddress + | otherwise = dummyAddress + testOps = + mkOps . CBOR.TokenUpdateTransaction . Seq.singleton $ + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = if tcSenderBalanceSufficient then mintAmt else excessiveAmt, + ttRecipient = actualRecipient, + ttMemo = tcMemo + } + memoEnergy = case tcMemo of + Nothing -> 0 + Just (CBOR.UntaggedMemo (Memo sbs)) + | l < 24 -> fromIntegral $ 6 + l + | l < 256 -> fromIntegral $ 7 + l + | otherwise -> fromIntegral $ 8 + l + where + l = BSS.length sbs + Just (CBOR.CBORMemo (Memo sbs)) + | l < 24 -> fromIntegral $ 8 + l + | l < 256 -> fromIntegral $ 9 + l + | otherwise -> fromIntegral $ 10 + l + where + l = BSS.length sbs + addressDeltaEnergy + | tcShortRecv = 0 + | otherwise = 9 + requiredEnergy = 642 + memoEnergy + addressDeltaEnergy + testEnergy = if tcEnergyInsufficient then requiredEnergy - 1 else requiredEnergy + assertTokenReject trr = + Helpers.assertRejectWithReason + . TokenUpdateTransactionFailed + . makeTokenModuleRejectReason pltName + . CBOR.encodeTokenRejectReason + $ trr + expectModuleState = + CBOR.tokenModuleStateToBytes $ + CBOR.TokenModuleState + { tmsName = CBOR.tipName params, + tmsMetadata = CBOR.tipMetadata params, + tmsGovernanceAccount = Just (CBOR.accountTokenHolder dummyAddress), + tmsPaused = Just tcPaused, + tmsAllowList = Just tcAllowList, + tmsDenyList = Just tcDenyList, + tmsMintable = Just False, + tmsBurnable = Just False, + tmsAdditional = mempty + } + expectSenderTokens sentOK = + [ Token + { tokenAccountState = + TokenAccountState + { moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = + if tcDenyList then Just tcSenderDeny else Nothing, + tmasAllowList = + if tcAllowList then Just tcSenderAllow else Nothing, + tmasAdditional = mempty + }, + balance = if sentOK then TokenAmount 0 0 else mintAmt + }, + tokenId = pltName + } + ] + expectDestTokens sentOK = + [ Token + { tokenAccountState = + TokenAccountState + { moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = + if tcDenyList then Just tcRecvDeny else Nothing, + tmasAllowList = + if tcAllowList then Just tcRecvAllow else Nothing, + tmasAdditional = mempty + }, + balance = if sentOK then mintAmt else TokenAmount 0 0 + }, + tokenId = pltName + } + | sentOK || tcRecvAllow || tcRecvDeny + ] + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount dummyAddress, + etmAmount = mintAmt + } + ] + result + }, + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = initOps + }, + metadata = makeDummyHeader dummyAddress 1 10_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result _ -> return $ Helpers.assertSuccess result + }, + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = testOps + }, + metadata = makeDummyHeader actualSenderAddress 2 testEnergy, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + senderIndex <- fromJust <$> BS.getAccount st dummyAddress + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + senderTokens <- queryAccountTokens senderIndex st + destTokens <- queryAccountTokens destIndex st + let postCheck sentOK = do + assertEqual "Used energy" testEnergy (Helpers.srUsedEnergy result) + assertEqual + "Sender tokens after transfer" + (expectSenderTokens sentOK) + senderTokens + assertEqual + "Recipient tokens after transfer" + (expectDestTokens sentOK) + destTokens + tokenInfo <- queryTokenInfo pltName st + return $ do + assertEqual + "Token info" + ( Right $ + TokenInfo + { tiTokenId = pltName, + tiTokenState = + TokenState + { tsTokenModuleRef = tokenModuleV0Ref, + tsDecimals = 0, + tsTotalSupply = mintAmt, + tsModuleState = expectModuleState + } + } + ) + tokenInfo + if + | tcEnergyInsufficient -> do + Helpers.assertRejectWithReason OutOfEnergy result + -- The full supplied energy will be used in the case of an + -- out-of-energy failure. + postCheck False + | tcRecvInvalid && demoteProtocolVersion spv >= Types.P11 -> do + assertTokenReject + CBOR.AddressNotFound + { trrOperationIndex = 0, + trrAddress = CBOR.accountTokenHolder actualRecipientAddress + } + result + postCheck False + | tcPaused -> do + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Nothing, + trrReason = Just "token operation transfer is paused" + } + result + postCheck False + | tcRecvInvalid -> do + assertTokenReject + CBOR.AddressNotFound + { trrOperationIndex = 0, + trrAddress = CBOR.accountTokenHolder actualRecipientAddress + } + result + postCheck False + | tcAllowList && not tcSenderAllow -> do + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder actualSenderAddress), + trrReason = Just "sender not in allow list" + } + result + postCheck False + | tcAllowList && not tcRecvAllow -> do + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder actualRecipientAddress), + trrReason = Just "recipient not in allow list" + } + result + postCheck False + | tcDenyList && tcSenderDeny -> do + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder actualSenderAddress), + trrReason = Just "sender in deny list" + } + result + postCheck False + | tcDenyList && tcRecvDeny -> do + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder actualRecipientAddress), + trrReason = Just "recipient in deny list" + } + result + postCheck False + | not tcSenderBalanceSufficient -> do + assertTokenReject + CBOR.TokenBalanceInsufficient + { trrOperationIndex = 0, + trrRequiredBalance = excessiveAmt, + trrAvailableBalance = mintAmt + } + result + postCheck False + | otherwise -> do + Helpers.assertSuccessWithEvents + [ TokenTransfer + { ettTokenId = pltName, + ettFrom = HolderAccount actualSenderAddress, + ettTo = HolderAccount actualRecipientAddress, + ettAmount = mintAmt, + ettMemo = CBOR.taggableMemoInner <$> tcMemo, + ettFromLock = Nothing, + ettToLock = Nothing + } + ] + result + postCheck True + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + +getTokenModuleState :: (BS.BlockStateQuery m) => TokenId -> BlockState m -> m (Either String CBOR.TokenModuleState) +getTokenModuleState tokenId st = + queryTokenInfo tokenId st >>= \case + Left e -> return . Left $ show e + Right r -> return $ CBOR.tokenModuleStateFromBytes $ LBS.fromStrict $ tsModuleState $ tiTokenState r + +testPauseUnpause :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + Assertion +testPauseUnpause spv = do + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + where + govAcct = CBOR.accountTokenHolder dummyAddress + mintAmt = TokenAmount 1000 0 + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Nothing, + tipDenyList = Nothing, + tipInitialSupply = Just mintAmt, + tipMintable = Nothing, + tipBurnable = Nothing, + tipAdditional = Map.empty + } + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + pltName = Types.TokenId $ fromString "PLT" + createPLT = Types.CreatePLT pltName tokenModuleV0Ref 0 tp + keys1 = [(0, [(0, dummyKP)])] + keys2 = [(0, [(0, Helpers.keyPairFromSeed 2)])] + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes . CBOR.TokenUpdateTransaction . Seq.fromList + mkUpdateTx sendAddr nonce nrg keys ops = + Runner.AccountTx + Runner.TJSON + { payload = Runner.TokenUpdate{tuTokenId = pltName, tuOperations = mkOps ops}, + metadata = makeDummyHeader sendAddr nonce nrg, + keys = keys + } + pauseEvent = + TokenModuleEvent + { etmeTokenId = pltName, + etmeType = TokenEventType "pause", + etmeDetails = CBOR.emptyEventDetails + } + unpauseEvent = + TokenModuleEvent + { etmeTokenId = pltName, + etmeType = TokenEventType "unpause", + etmeDetails = CBOR.emptyEventDetails + } + checkEnergyStateEvents :: Energy -> Bool -> [Event] -> Helpers.TransactionAssertion pv + checkEnergyStateEvents nrg expectPaused evts = \result ust -> do + st <- BS.freezeBlockState ust + tms <- getTokenModuleState pltName st + return $ do + assertEqual "Used energy" nrg (Helpers.srUsedEnergy result) + assertEqual "Pause state" (Right (Just expectPaused)) (CBOR.tmsPaused <$> tms) + Helpers.assertSuccessWithEvents evts result + assertTokenReject trr = + Helpers.assertRejectWithReason + . TokenUpdateTransactionFailed + . makeTokenModuleRejectReason pltName + . CBOR.encodeTokenRejectReason + $ trr + checkEnergyStateReason nrg expectPaused rr = \result ust -> do + st <- BS.freezeBlockState ust + tms <- getTokenModuleState pltName st + return $ do + assertEqual "Used energy" nrg (Helpers.srUsedEnergy result) + assertEqual "Pause state" (Right (Just expectPaused)) (CBOR.tmsPaused <$> tms) + assertTokenReject rr result + transactionsAndAssertions = + [ -- Initialise the token. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount dummyAddress, + etmAmount = mintAmt + } + ] + result + }, + -- Pause from gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 1 1000 keys1 [CBOR.TokenPause], + biaaAssertion = checkEnergyStateEvents 528 True [pauseEvent] + }, + -- Pause from gov account while already paused (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 2 1000 keys1 [CBOR.TokenPause], + biaaAssertion = checkEnergyStateEvents 528 True [pauseEvent] + }, + -- Unpause from gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 3 1000 keys1 [CBOR.TokenUnpause], + biaaAssertion = checkEnergyStateEvents 530 False [unpauseEvent] + }, + -- Unpause from gov account while already unpaused (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 4 1000 keys1 [CBOR.TokenUnpause], + biaaAssertion = checkEnergyStateEvents 530 False [unpauseEvent] + }, + -- Pause from an alias of the gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx (distinctAlias dummyAddress) 5 1000 keys1 [CBOR.TokenPause], + biaaAssertion = checkEnergyStateEvents 528 True [pauseEvent] + }, + -- Unpause from the gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 6 1000 keys1 [CBOR.TokenUnpause], + biaaAssertion = checkEnergyStateEvents 530 False [unpauseEvent] + }, + -- Pause twice from the gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 7 1000 keys1 [CBOR.TokenPause, CBOR.TokenPause], + biaaAssertion = checkEnergyStateEvents 586 True [pauseEvent, pauseEvent] + }, + -- Unpause from an alias of the gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx (distinctAlias dummyAddress) 8 1000 keys1 [CBOR.TokenUnpause], + biaaAssertion = checkEnergyStateEvents 530 False [unpauseEvent] + }, + -- Unpause, pause, unpause from the gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 9 1000 keys1 [CBOR.TokenUnpause, CBOR.TokenPause, CBOR.TokenUnpause], + biaaAssertion = checkEnergyStateEvents 648 False [unpauseEvent, pauseEvent, unpauseEvent] + }, + -- Pause from a non-gov account (fails: not permitted). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress2 1 1000 keys2 [CBOR.TokenPause], + biaaAssertion = + checkEnergyStateReason 528 False $ + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder dummyAddress2), + trrReason = Just $ notAuthorizedReason spv + } + }, + -- Pause and transfer from gov account (fails: transfer not permitted while paused). + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 10 1000 keys1 $ + [ CBOR.TokenPause, + CBOR.TokenTransfer + CBOR.TokenTransferBody + { ttRecipient = CBOR.accountTokenHolder dummyAddress, + ttMemo = Nothing, + ttAmount = TokenAmount 10 0 + } + ], + biaaAssertion = + checkEnergyStateReason 708 False $ + CBOR.OperationNotPermitted + { trrOperationIndex = 1, + trrAddressNotPermitted = Nothing, + trrReason = Just "token operation transfer is paused" + } + }, + -- Unpause, pause from gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 11 1000 keys1 [CBOR.TokenUnpause, CBOR.TokenPause], + biaaAssertion = checkEnergyStateEvents 588 True [unpauseEvent, pauseEvent] + }, + -- Unpause, transfer, pause from gov account (OK). + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 12 1000 keys1 $ + [ CBOR.TokenUnpause, + CBOR.TokenTransfer + CBOR.TokenTransferBody + { ttRecipient = CBOR.accountTokenHolder dummyAddress, + ttMemo = Nothing, + ttAmount = TokenAmount 10 0 + }, + CBOR.TokenPause + ], + biaaAssertion = + checkEnergyStateEvents 768 True $ + [ unpauseEvent, + TokenTransfer + { ettTokenId = pltName, + ettFrom = HolderAccount dummyAddress, + ettTo = HolderAccount dummyAddress, + ettAmount = TokenAmount 10 0, + ettMemo = Nothing, + ettFromLock = Nothing, + ettToLock = Nothing + }, + pauseEvent + ] + }, + -- Unpause from non-gov account (fails: not permitted). + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress2 2 1000 keys2 [CBOR.TokenUnpause], + biaaAssertion = + checkEnergyStateReason 530 True $ + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder dummyAddress2), + trrReason = Just $ notAuthorizedReason spv + } + } + ] + +notAuthorizedReason :: (IsProtocolVersion pv, PVSupportsPLT pv) => SProtocolVersion pv -> Text +notAuthorizedReason spv = case spv of + SP9 -> "sender is not the token governance account" + SP10 -> "sender is not the token governance account" + _ -> "sender is not authorized to perform the operation for this token" + +testMintBurn :: forall pv. (IsProtocolVersion pv, PVSupportsPLT pv) => SProtocolVersion pv -> Bool -> Bool -> Assertion +testMintBurn spv mintEnabled burnEnabled = do + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + where + govAcct = CBOR.accountTokenHolder dummyAddress + mintAmt = TokenAmount 1000 0 params = CBOR.TokenInitializationParameters { tipName = Just "Protocol-level token", tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", - tipGovernanceAccount = Just dummyCborAccountAddress, - tipAllowList = Just True, - tipDenyList = Just False, - tipInitialSupply = Nothing, - tipMintable = Just True, - tipBurnable = Just True, + tipGovernanceAccount = Just govAcct, + tipAllowList = Just True, -- NB: allow and deny list should not affect mint/burn + tipDenyList = Just True, + tipInitialSupply = Just mintAmt, + tipMintable = Just mintEnabled, + tipBurnable = Just burnEnabled, tipAdditional = Map.empty } - tp = Types.TokenParameter $ BSS.toShort $ CBOR.tokenInitializationParametersToBytes params - createPLT = Types.CreatePLT gtu tokenModuleV0Ref 0 tp - plt = Types.CreatePLTUpdatePayload createPLT - gtuEvent = TokenCreated{etcPayload = createPLT} - -- This is CBOR-encoding of {"cause": "DeserialiseFailure 0 \"end of input\""} - cborFail = Types.TokenEventDetails $ BSS.pack [161, 101, 99, 97, 117, 115, 101, 120, 35, 68, 101, 115, 101, 114, 105, 97, 108, 105, 115, 101, 70, 97, 105, 108, 117, 114, 101, 32, 48, 32, 34, 101, 110, 100, 32, 111, 102, 32, 105, 110, 112, 117, 116, 34] - errType = Types.TokenEventType $ fromString "deserializationFailure" + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + pltName = Types.TokenId $ fromString "PLT" + createPLT = Types.CreatePLT pltName tokenModuleV0Ref 0 tp + keys1 = [(0, [(0, dummyKP)])] + keys2 = [(0, [(0, Helpers.keyPairFromSeed 2)])] + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes . CBOR.TokenUpdateTransaction . Seq.fromList + mkUpdateTx sendAddr nonce nrg keys ops = + Runner.AccountTx + Runner.TJSON + { payload = Runner.TokenUpdate{tuTokenId = pltName, tuOperations = mkOps ops}, + metadata = makeDummyHeader sendAddr nonce nrg, + keys = keys + } + mintEventFor addr amt = + TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount addr, + etmAmount = amt + } + mintEvent = mintEventFor dummyAddress + burnEventFor addr amt = + TokenBurn + { etbTokenId = pltName, + etbTarget = HolderAccount addr, + etbAmount = amt + } + burnEvent = burnEventFor dummyAddress + expectGovTokens amount = + [ Token + { tokenId = pltName, + tokenAccountState = + TokenAccountState + { balance = amount, + moduleAccountState = + Just . CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Just False, + tmasAllowList = Just False, + tmasAdditional = mempty + } + } + } + ] + checkEnergyStateEvents :: Bool -> Energy -> Integer -> Integer -> Integer -> [Event] -> Helpers.TransactionAssertion pv + checkEnergyStateEvents isMint nrg mintTotal burnTotal delta evts = \result ust -> do + let opEnabled = if isMint then mintEnabled else burnEnabled + st <- BS.freezeBlockState ust + mSupply <- fmap (tsTotalSupply . tiTokenState) <$> queryTokenInfo pltName st + govIndex <- fromJust <$> BS.getAccount st dummyAddress + govAccountTokens <- queryAccountTokens govIndex st + let cnd b a = if b then a else 0 + let expectSupply = + TokenAmount + (fromInteger $ 1000 + cnd opEnabled delta + cnd mintEnabled mintTotal - cnd burnEnabled burnTotal) + 0 + return $ do + assertEqual "Used energy" nrg (Helpers.srUsedEnergy result) + assertEqual "Total supply" (Right expectSupply) mSupply + assertEqual "Governance account tokens" (expectGovTokens expectSupply) govAccountTokens + if opEnabled + then Helpers.assertSuccessWithEvents evts result + else + assertTokenReject + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = if isMint then "mint" else "burn", + trrReason = Just $ "feature not enabled" + } + result + assertTokenReject trr = + Helpers.assertRejectWithReason + . TokenUpdateTransactionFailed + . makeTokenModuleRejectReason pltName + . CBOR.encodeTokenRejectReason + $ trr + checkEnergyStateReason :: Energy -> Integer -> Integer -> CBOR.TokenRejectReason -> Helpers.TransactionAssertion pv + checkEnergyStateReason nrg mintTotal burnTotal rr = \result ust -> do + st <- BS.freezeBlockState ust + mSupply <- fmap (tsTotalSupply . tiTokenState) <$> queryTokenInfo pltName st + govIndex <- fromJust <$> BS.getAccount st dummyAddress + govAccountTokens <- queryAccountTokens govIndex st + let cnd b a = if b then a else 0 + let expectSupply = + TokenAmount + (fromInteger $ 1000 + cnd mintEnabled mintTotal - cnd burnEnabled burnTotal) + 0 + return $ do + assertEqual "Used energy" nrg (Helpers.srUsedEnergy result) + assertEqual "Total supply" (Right expectSupply) mSupply + assertEqual "Governance account tokens" (expectGovTokens expectSupply) govAccountTokens + assertTokenReject rr result + transactionsAndAssertions = + [ -- Initialise the token. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> do + return $ + Helpers.assertSuccessWithEvents + [TokenCreated{etcPayload = createPLT}, mintEvent mintAmt] + result + }, + -- Mint 50 from gov acct (OK if mint enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 1 1000 keys1 $ + [CBOR.TokenMint (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateEvents True 539 0 0 50 $ + [mintEvent (TokenAmount 50 0)] + }, + -- Burn 50 from gov acct (OK if burn enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 2 1000 keys1 $ + [CBOR.TokenBurn (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateEvents False 539 50 0 (-50) $ + [burnEvent (TokenAmount 50 0)] + }, + -- Mint 50 from gov acct with alias (OK if mint enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx (distinctAlias dummyAddress) 3 1000 keys1 $ + [CBOR.TokenMint (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateEvents True 539 50 50 50 $ + [mintEventFor (distinctAlias dummyAddress) (TokenAmount 50 0)] + }, + -- Burn 50 from gov acct with alias (OK if burn enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx (distinctAlias dummyAddress) 4 1000 keys1 $ + [CBOR.TokenBurn (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateEvents False 539 100 50 (-50) $ + [burnEventFor (distinctAlias dummyAddress) (TokenAmount 50 0)] + }, + -- Mint 50 from non-gov acct (fails: not permitted) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress2 1 1000 keys2 $ + [CBOR.TokenMint (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateReason 539 100 100 $ + if mintEnabled || demoteProtocolVersion spv < Types.P11 + then + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder dummyAddress2), + trrReason = Just $ notAuthorizedReason spv + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "mint", + trrReason = Just $ "feature not enabled" + } + }, + -- Burn 50 from non-gov acct (fails: not permitted) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress2 2 1000 keys2 $ + [CBOR.TokenBurn (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateReason 539 100 100 $ + if burnEnabled || demoteProtocolVersion spv < Types.P11 + then + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder dummyAddress2), + trrReason = Just $ notAuthorizedReason spv + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "burn", + trrReason = Just $ "feature not enabled" + } + }, + -- Mint too much from gov acct (fails: would overflow, or not enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 5 1000 keys1 $ + [CBOR.TokenMint (TokenAmount (maxBound - 100) 0)], + biaaAssertion = + checkEnergyStateReason 546 100 100 $ + if mintEnabled + then + CBOR.MintWouldOverflow + { trrOperationIndex = 0, + trrRequestedAmount = TokenAmount (maxBound - 100) 0, + trrMaxRepresentableAmount = TokenAmount maxBound 0, + trrCurrentSupply = TokenAmount (1100 - (if burnEnabled then 100 else 0)) 0 + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "mint", + trrReason = Just $ "feature not enabled" + } + }, + -- Burn too much from gov acct (fails: balance insufficient, or not enabled) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 6 1000 keys1 $ + [CBOR.TokenBurn (TokenAmount (1101) 0)], + biaaAssertion = + checkEnergyStateReason 540 100 100 $ + if burnEnabled + then + CBOR.TokenBalanceInsufficient + { trrOperationIndex = 0, + trrRequiredBalance = TokenAmount 1101 0, + trrAvailableBalance = TokenAmount (900 + (if mintEnabled then 100 else 0)) 0 + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "burn", + trrReason = Just $ "feature not enabled" + } + }, + -- Pause the token (OK) + Helpers.BlockItemAndAssertion + { biaaTransaction = mkUpdateTx dummyAddress 7 1000 keys1 [CBOR.TokenPause], + biaaAssertion = \result _ -> + return $ + Helpers.assertSuccessWithEvents + [ TokenModuleEvent + { etmeTokenId = pltName, + etmeType = TokenEventType "pause", + etmeDetails = CBOR.emptyEventDetails + } + ] + result + }, + -- Mint from gov acct while paused (fails: operation not permitted) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 8 1000 keys1 $ + [CBOR.TokenMint (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateReason 539 100 100 $ + if mintEnabled || demoteProtocolVersion spv < Types.P11 + then + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Nothing, + trrReason = Just "token operation mint is paused" + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "mint", + trrReason = Just $ "feature not enabled" + } + }, + -- Burn from gov acct while paused (fails: operation not permitted) + Helpers.BlockItemAndAssertion + { biaaTransaction = + mkUpdateTx dummyAddress 9 1000 keys1 $ + [CBOR.TokenBurn (TokenAmount 50 0)], + biaaAssertion = + checkEnergyStateReason 539 100 100 $ + if burnEnabled || demoteProtocolVersion spv < Types.P11 + then + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Nothing, + trrReason = Just "token operation burn is paused" + } + else + CBOR.UnsupportedOperation + { trrOperationIndex = 0, + trrOperationType = "burn", + trrReason = Just $ "feature not enabled" + } + } + ] + +-- | Test that a token transfer is accepted when the recipient address has no coin info. +-- Verifies that the resulting balances are correct after the transfer. +testNoCoinInfoTransfer :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + String -> + Spec +testNoCoinInfoTransfer _ pvString = + specify (pvString ++ ": Token transfer with no coin info in recipient address") $ do + let govAcct = CBOR.accountTokenHolder dummyAddress + -- Recipient address without coin info (the "short" / untagged form). + recptShort = CBOR.accountTokenHolderShort dummyAddress2 + mintAmt = TokenAmount 100 0 + transferAmt = TokenAmount 40 0 + remainderAmt = TokenAmount 60 0 + pltName = Types.TokenId $ fromString "PLT" + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just False, + tipDenyList = Just False, + tipInitialSupply = Just mintAmt, + tipMintable = Nothing, + tipBurnable = Nothing, + tipAdditional = Map.empty + } + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + createPLT = Types.CreatePLT pltName tokenModuleV0Ref 0 tp + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes + transferOps = + mkOps $ + CBOR.TokenUpdateTransaction $ + Seq.singleton $ + CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = transferAmt, + ttRecipient = recptShort, + ttMemo = Nothing + } + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount dummyAddress, + etmAmount = mintAmt + } + ] + result + }, + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = transferOps + }, + metadata = makeDummyHeader dummyAddress 1 10_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + senderIndex <- fromJust <$> BS.getAccount st dummyAddress + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + senderTokens <- queryAccountTokens senderIndex st + destTokens <- queryAccountTokens destIndex st + return $ do + -- Transfer to a short-form (no coin info) address is accepted. + Helpers.assertSuccessWithEvents + [ TokenTransfer + { ettTokenId = pltName, + ettFrom = HolderAccount dummyAddress, + ettTo = HolderAccount dummyAddress2, + ettAmount = transferAmt, + ettMemo = Nothing, + ettFromLock = Nothing, + ettToLock = Nothing + } + ] + result + -- Resulting balances are correct. + assertEqual + "Sender balance after transfer" + [Token{tokenId = pltName, tokenAccountState = TokenAccountState{balance = remainderAmt, moduleAccountState = Just emptyModuleAccountState}}] + senderTokens + assertEqual + "Recipient balance after transfer" + [Token{tokenId = pltName, tokenAccountState = TokenAccountState{balance = transferAmt, moduleAccountState = Just emptyModuleAccountState}}] + destTokens + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions + where + emptyModuleAccountState = + CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Nothing, + tmasAllowList = Nothing, + tmasAdditional = mempty + } + +-- | Test that allow/deny list operations are accepted when the target address has no coin info. +-- Verifies that: +-- - Operations with no-coin-info addresses are accepted. +-- - Output events carry coin info exactly as specified in the operation (i.e. absent if absent). +-- - Reject reasons always report addresses with coin info set. +-- - The resulting account state correctly reflects list membership. +testNoCoinInfoAllowDenyList :: + forall pv. + (IsProtocolVersion pv, PVSupportsPLT pv) => + SProtocolVersion pv -> + String -> + Spec +testNoCoinInfoAllowDenyList _ pvString = + specify (pvString ++ ": Allow/deny list operations with no coin info in target address") $ do + let govAcct = CBOR.accountTokenHolder dummyAddress + -- Target address without coin info (the "short" / untagged form). + targetShort = CBOR.accountTokenHolderShort dummyAddress2 + mintAmt = TokenAmount 100 0 + pltName = Types.TokenId $ fromString "PLT" + params = + CBOR.TokenInitializationParameters + { tipName = Just "Protocol-level token", + tipMetadata = Just $ CBOR.createTokenMetadataUrl "https://plt.token", + tipGovernanceAccount = Just govAcct, + tipAllowList = Just True, + tipDenyList = Just True, + tipInitialSupply = Just mintAmt, + tipMintable = Nothing, + tipBurnable = Nothing, + tipAdditional = Map.empty + } + tp = Types.rawCborFromBytes $ CBOR.tokenInitializationParametersToBytes params + createPLT = Types.CreatePLT pltName tokenModuleV0Ref 0 tp + mkOps = Types.rawCborFromBytes . CBOR.tokenUpdateTransactionToBytes . CBOR.TokenUpdateTransaction . Seq.fromList + -- Helper to build a TokenModuleEvent for a list update. + listEvent evtType target = + TokenModuleEvent + { etmeTokenId = pltName, + etmeType = TokenEventType evtType, + etmeDetails = CBOR.encodeTargetDetails target + } + assertTokenReject trr = + Helpers.assertRejectWithReason + . TokenUpdateTransactionFailed + . makeTokenModuleRejectReason pltName + . CBOR.encodeTokenRejectReason + $ trr + -- Expected account state for dummyAddress2 after being added to both lists. + expectTargetAccountState onAllow onDeny = + Just $ + CBOR.tokenModuleAccountStateToBytes $ + CBOR.TokenModuleAccountState + { tmasDenyList = Just onDeny, + tmasAllowList = Just onAllow, + tmasAdditional = mempty + } + transactionsAndAssertions :: [Helpers.BlockItemAndAssertion pv] + transactionsAndAssertions = + [ -- Create the PLT. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.ChainUpdateTx $ + Runner.ChainUpdateTransaction + { ctSeqNumber = 1, + ctEffectiveTime = 0, + ctTimeout = DummyData.dummyMaxTransactionExpiryTime, + ctPayload = Types.CreatePLTUpdatePayload createPLT, + ctKeys = [(0, DummyData.dummyAuthorizationKeyPair)] + }, + biaaAssertion = \result _ -> + return $ + Helpers.assertSuccessWithEvents + [ TokenCreated{etcPayload = createPLT}, + TokenMint + { etmTokenId = pltName, + etmTarget = HolderAccount dummyAddress, + etmAmount = mintAmt + } + ] + result + }, + -- Add dummyAddress2 to the allow list using a short-form (no coin info) address. + -- The operation is accepted and the output event target has no coin info. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = mkOps [CBOR.TokenAddAllowList targetShort] + }, + metadata = makeDummyHeader dummyAddress 1 10_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + destTokens <- queryAccountTokens destIndex st + return $ do + -- Accepted; event carries the target address exactly as in the + -- operation (no coin info, since the operation had none). + Helpers.assertSuccessWithEvents + [listEvent "addAllowList" targetShort] + result + -- The resulting account state shows the account is on the allow list. + assertEqual + "Target account state after addAllowList" + [Token{tokenId = pltName, tokenAccountState = TokenAccountState{balance = TokenAmount 0 0, moduleAccountState = expectTargetAccountState True False}}] + destTokens + }, + -- Attempt a transfer from dummyAddress (not on the allow list) to + -- dummyAddress2 (which is on the allow list), using the short-form recipient + -- address to also exercise no-coin-info in a rejected operation. + -- This is rejected; the reject reason reports the sender address with coin info set. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = + mkOps + [ CBOR.TokenTransfer $ + CBOR.TokenTransferBody + { ttAmount = TokenAmount 10 0, + ttRecipient = targetShort, + ttMemo = Nothing + } + ] + }, + metadata = makeDummyHeader dummyAddress 2 10_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result _ -> + return $ + -- Rejected: sender (dummyAddress) is not on the allow list. + -- The reject reason uses the full-form address (coin info set). + assertTokenReject + CBOR.OperationNotPermitted + { trrOperationIndex = 0, + trrAddressNotPermitted = Just (CBOR.accountTokenHolder dummyAddress), + trrReason = Just "sender not in allow list" + } + result + }, + -- Add dummyAddress2 to the deny list using a short-form (no coin info) address. + -- The operation is accepted and the output event target has no coin info. + Helpers.BlockItemAndAssertion + { biaaTransaction = + Runner.AccountTx $ + Runner.TJSON + { payload = + Runner.TokenUpdate + { tuTokenId = pltName, + tuOperations = mkOps [CBOR.TokenAddDenyList targetShort] + }, + metadata = makeDummyHeader dummyAddress 3 10_000, + keys = [(0, [(0, dummyKP)])] + }, + biaaAssertion = \result ust -> do + st <- BS.freezeBlockState ust + destIndex <- fromJust <$> BS.getAccount st dummyAddress2 + destTokens <- queryAccountTokens destIndex st + return $ do + -- Accepted; event carries the target address exactly as in the + -- operation (no coin info, since the operation had none). + Helpers.assertSuccessWithEvents + [listEvent "addDenyList" targetShort] + result + -- The resulting account state shows the account is on both lists. + assertEqual + "Target account state after addDenyList" + [Token{tokenId = pltName, tokenAccountState = TokenAccountState{balance = TokenAmount 0 0, moduleAccountState = expectTargetAccountState True True}}] + destTokens + } + ] + Helpers.runSchedulerTestAssertIntermediateStates + @pv + Helpers.defaultTestConfig + initialBlockState + transactionsAndAssertions tests :: Spec tests = - describe "Token holder transactions" $ - sequence_ $ - Helpers.forEveryProtocolVersion testCases + parallel $ + describe "Token holder transactions" $ + sequence_ $ + Helpers.forEveryProtocolVersion testCases where testCases :: forall pv. (IsProtocolVersion pv) => SProtocolVersion pv -> String -> Spec testCases spv pvString = case sSupportsPLT (sAccountVersionFor spv) of - STrue -> testTokenHolder spv pvString + STrue -> describe pvString $ do + testNonExistingToken spv pvString + testDeserializationFailure spv pvString + testTwoOperations spv pvString + testRollback spv pvString + it "PLT transfers" $ withMaxSuccess 500 $ testTransfer spv + it "Pause/unpause" $ testPauseUnpause spv + describe "Mint/burn" $ do + it "mint enabled, burn enabled" $ testMintBurn spv True True + it "mint enabled, burn disabled" $ testMintBurn spv True False + it "mint disabled, burn enabled" $ testMintBurn spv False True + it "mint disabled, burn disabled" $ testMintBurn spv False False + testNoCoinInfoTransfer spv pvString + testNoCoinInfoAllowDenyList spv pvString SFalse -> return () diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TokenModule.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TokenModule.hs index 433f3196e9..d5029a5af7 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TokenModule.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TokenModule.hs @@ -46,7 +46,7 @@ import Concordium.GlobalState.Persistent.BlockState import qualified Concordium.GlobalState.Persistent.BlockState as BS import Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens import Concordium.Scheduler.DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as EI +import qualified Concordium.Scheduler.Environment as EI import Concordium.Scheduler.ProtocolLevelTokens.Kernel import Concordium.Scheduler.ProtocolLevelTokens.Module ( InitializeTokenError (..), @@ -336,7 +336,7 @@ testInitializeToken = describe "initializeToken" $ do abortPLTError $ ITEDeserializationFailure "DeserialiseFailure 0 \"end of input\"" assertTrace - (initializeToken (TokenParameter mempty)) + (initializeToken (rawCborFromBytes mempty)) trace -- In this example, a parameter is missing from the required initialization parameters it "invalid parameters: missing parameter" $ do @@ -358,7 +358,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just True, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = abortPLTError $ @@ -386,7 +386,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Nothing, tipAdditional = Map.fromList [("_param1", CBOR.TString "extravalue1")] } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = abortPLTError $ @@ -414,7 +414,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Nothing, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token") :-> Just False) @@ -444,7 +444,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just True, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token") :-> Just False) @@ -477,7 +477,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just False, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token2") :-> Just False) @@ -510,7 +510,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just False, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token2") :-> Just False) @@ -543,7 +543,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just False, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token2") :-> Just False) @@ -575,7 +575,7 @@ testInitializeToken = describe "initializeToken" $ do tipBurnable = Just False, tipAdditional = Map.empty } - tokenParam = TokenParameter $ SBS.toShort $ tokenInitializationParametersToBytes params + tokenParam = rawCborFromBytes $ tokenInitializationParametersToBytes params trace :: Trace (PLTCall InitializeTokenError AccountIndex) () trace = (PLTU (setModuleStateCall "name" $ Just "Protocol-level token2") :-> Just False) @@ -598,7 +598,7 @@ testExecuteTokenUpdateTransactionTransfer = describe "executeTokenUpdateTransact abortPLTError . encodeTokenRejectReason $ DeserializationFailure (Just "DeserialiseFailure 0 \"end of input\"") assertTrace - (executeTokenUpdateTransaction (sender 0) (TokenParameter mempty)) + (executeTokenUpdateTransaction (sender 0) (rawCborFromBytes mempty)) trace it "empty operations" $ do let transaction = TokenUpdateTransaction Seq.empty @@ -837,7 +837,7 @@ testExecuteTokenUpdateTransactionTransfer = describe "executeTokenUpdateTransact :>>: ( abortPLTError . encodeTokenRejectReason $ OperationNotPermitted { trrOperationIndex = 0, - trrAddressNotPermitted = Just receiver1, + trrAddressNotPermitted = Just $ accountTokenHolder $ chaAccount receiver1, trrReason = Just "recipient not in allow list" } ) @@ -903,7 +903,7 @@ testExecuteTokenUpdateTransactionTransfer = describe "executeTokenUpdateTransact :>>: ( abortPLTError . encodeTokenRejectReason $ OperationNotPermitted { trrOperationIndex = 0, - trrAddressNotPermitted = Just receiver1, + trrAddressNotPermitted = Just $ accountTokenHolder $ chaAccount receiver1, trrReason = Just "recipient in deny list" } ) @@ -965,7 +965,7 @@ testExecuteTokenUpdateTransactionTransfer = describe "executeTokenUpdateTransact longMemo = Memo $ SBS.replicate maxMemoSize 60 badMemo = Memo $ SBS.replicate (maxMemoSize + 1) 60 mkTransferOp ttAmount ttRecipient ttMemo = TokenTransfer TokenTransferBody{..} - encodeTransaction = TokenParameter . SBS.toShort . tokenUpdateTransactionToBytes + encodeTransaction = rawCborFromBytes . tokenUpdateTransactionToBytes sender ai = TransactionContext (AccountIndex ai) (dummyAccountAddress $ fromIntegral ai) testExecuteTokenUpdateTransactionMintBurnPause :: Spec @@ -976,7 +976,7 @@ testExecuteTokenUpdateTransactionMintBurnPause = describe "executeTokenUpdateTra abortPLTError . encodeTokenRejectReason $ DeserializationFailure (Just "DeserialiseFailure 0 \"end of input\"") assertTrace - (executeTokenUpdateTransaction (sender 0) (TokenParameter mempty)) + (executeTokenUpdateTransaction (sender 0) (rawCborFromBytes mempty)) trace it "empty operations" $ do let transaction = TokenUpdateTransaction Seq.empty @@ -1214,7 +1214,7 @@ testExecuteTokenUpdateTransactionMintBurnPause = describe "executeTokenUpdateTra :>>: Done () assertTrace (executeTokenUpdateTransaction (sender 0) (encodeTransaction transaction)) trace where - encodeTransaction = TokenParameter . SBS.toShort . tokenUpdateTransactionToBytes + encodeTransaction = rawCborFromBytes . tokenUpdateTransactionToBytes sender ai = TransactionContext (AccountIndex ai) (dummyAccountAddress $ fromIntegral ai) data AddRemove = Add | Remove @@ -1289,7 +1289,7 @@ testLists = do ) assertTrace (executeTokenUpdateTransaction (sender 0) (encodeTransaction transaction)) trace where - encodeTransaction = TokenParameter . SBS.toShort . tokenUpdateTransactionToBytes + encodeTransaction = rawCborFromBytes . tokenUpdateTransactionToBytes receiver1 = CborAccountAddress (dummyAccountAddress 1) Nothing receiver2 = CborAccountAddress (dummyAccountAddress 2) (Just CoinInfoConcordium) ltcFeature :: ListTestConf -> TokenStateKey @@ -1333,11 +1333,7 @@ testQueryTokenModuleState = describe "queryTokenModuleState" $ do it "Example 1" $ do let metadata = createTokenMetadataUrl "some URL" - governanceAccount = - CborAccountAddress - { chaAccount = dummyAccountAddress 1, - chaCoinInfo = Nothing - } + governanceAccount = accountTokenHolder $ dummyAccountAddress 1 trace :: Trace (PLTCall QueryTokenError AccountIndex) BS.ByteString trace = (PLTQ (getModuleStateCall "name") :-> Just "My protocol-level token") @@ -1367,11 +1363,7 @@ testQueryTokenModuleState = describe "queryTokenModuleState" $ do assertTrace queryTokenModuleState trace it "Example 2" $ do let metadata = createTokenMetadataUrlWithSha256 "https://token.metadata" $ SHA256.hashShort $ SBS.pack $ replicate 32 0 - governanceAccount = - CborAccountAddress - { chaAccount = dummyAccountAddress 1, - chaCoinInfo = Nothing - } + governanceAccount = accountTokenHolder $ dummyAccountAddress 1 trace :: Trace (PLTCall QueryTokenError AccountIndex) BS.ByteString trace = (PLTQ (getModuleStateCall "name") :-> Just "Another PLT") @@ -1627,9 +1619,9 @@ testTokenOutOfEnergy = describe "tokenOutOfEnergy" $ do TokenUpdateTransaction . Seq.fromList $ [mkTransferOp amt10'000 receiver1 Nothing, mkTransferOp amt10'000 receiver1 Nothing] encodeTxTH = - TokenParameter . SBS.toShort . tokenUpdateTransactionToBytes + rawCborFromBytes . tokenUpdateTransactionToBytes encodeTxGV = - TokenParameter . SBS.toShort . tokenUpdateTransactionToBytes + rawCborFromBytes . tokenUpdateTransactionToBytes receiver1 = CborAccountAddress (dummyAccountAddress 0) Nothing amt10'000 = TokenAmount 10_000 3 mkMintOp toMintAmount = TokenMint{..} @@ -1695,7 +1687,7 @@ testTokenOutOfEnergy = describe "tokenOutOfEnergy" $ do } initialBlockState :: - (Types.IsProtocolVersion pv, PVSupportsPLT pv) => + (Types.IsProtocolVersion pv, PVSupportsHaskellManagedPLT pv) => Helpers.PersistentBSM pv (BS.HashedPersistentBlockState pv) initialBlockState = do bs0 <- diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TransactionExpirySpec.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TransactionExpirySpec.hs index bff0a5a666..5105d2553e 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TransactionExpirySpec.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TransactionExpirySpec.hs @@ -18,7 +18,7 @@ import Concordium.GlobalState.DummyData import qualified Concordium.GlobalState.Persistent.BlockState as BS import qualified Concordium.Scheduler as Sch import Concordium.Scheduler.DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as Types +import qualified Concordium.Scheduler.Environment as Types import Concordium.Scheduler.Runner import qualified Concordium.Scheduler.Types as Types import Concordium.Types.Accounts ( diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TransactionGroupingSpec2.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TransactionGroupingSpec2.hs index 65003a4073..188de7e7f5 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TransactionGroupingSpec2.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TransactionGroupingSpec2.hs @@ -19,7 +19,7 @@ import qualified Concordium.Crypto.SignatureScheme as SigScheme import qualified Concordium.GlobalState.Persistent.BlockState as BS import qualified Concordium.Scheduler as Sch import Concordium.Scheduler.DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as Types +import qualified Concordium.Scheduler.Environment as Types import Concordium.Scheduler.Runner import Concordium.Scheduler.Types (Amount, Energy, FailureKind (..), Nonce) import qualified Concordium.Scheduler.Types as Types diff --git a/concordium-consensus/tests/scheduler/SchedulerTests/TransfersWithScheduleTest.hs b/concordium-consensus/tests/scheduler/SchedulerTests/TransfersWithScheduleTest.hs index e3de27830e..58969ccc86 100644 --- a/concordium-consensus/tests/scheduler/SchedulerTests/TransfersWithScheduleTest.hs +++ b/concordium-consensus/tests/scheduler/SchedulerTests/TransfersWithScheduleTest.hs @@ -17,7 +17,7 @@ import qualified Concordium.GlobalState.Persistent.BlockState as BS import Concordium.ID.Types as Types import qualified Concordium.Scheduler as Sch import Concordium.Scheduler.DummyData -import qualified Concordium.Scheduler.EnvironmentImplementation as Types +import qualified Concordium.Scheduler.Environment as Types import qualified Concordium.Scheduler.Runner as Runner import Concordium.Scheduler.Types import qualified Concordium.Scheduler.Types as Types diff --git a/concordium-consensus/tests/scheduler/Spec.hs b/concordium-consensus/tests/scheduler/Spec.hs index 59daf5337d..a025151b2c 100644 --- a/concordium-consensus/tests/scheduler/Spec.hs +++ b/concordium-consensus/tests/scheduler/Spec.hs @@ -12,11 +12,14 @@ import qualified SchedulerTests.InitContextTest (tests) import qualified SchedulerTests.InitPoliciesTest (tests) import qualified SchedulerTests.InitialAccountCreationSpec (tests) import qualified SchedulerTests.MaxIncomingAmountsTest (tests) +import qualified SchedulerTests.MaxLockDurationUpdate (tests) +import qualified SchedulerTests.MetaUpdateTransactions (tests) import qualified SchedulerTests.Payday (tests) import qualified SchedulerTests.RandomBakerTransactions (tests) import qualified SchedulerTests.ReceiveContextTest (tests) import qualified SchedulerTests.RejectReasons (tests) import qualified SchedulerTests.RejectReasonsRustContract (tests) +import qualified SchedulerTests.RustScheduler (tests) import qualified SchedulerTests.SimpleTransferSpec (tests) import qualified SchedulerTests.SimpleTransfersTest (tests) import qualified SchedulerTests.SponsoredTransactions (tests) @@ -119,4 +122,7 @@ main = hspec $ do SchedulerTests.KonsensusV1.EpochTransition.tests SchedulerTests.TokenModule.tests SchedulerTests.TokenCreation.tests + SchedulerTests.MaxLockDurationUpdate.tests SchedulerTests.TokenHolderTransactions.tests + SchedulerTests.MetaUpdateTransactions.tests + SchedulerTests.RustScheduler.tests diff --git a/concordium-node/Cargo.lock b/concordium-node/Cargo.lock index 0b44ecce3a..0ed2c86751 100644 --- a/concordium-node/Cargo.lock +++ b/concordium-node/Cargo.lock @@ -819,7 +819,7 @@ dependencies = [ [[package]] name = "concordium_node" -version = "10.0.9" +version = "11.2.2" dependencies = [ "anyhow", "app_dirs2", diff --git a/concordium-node/Cargo.toml b/concordium-node/Cargo.toml index a22a096a6c..14e41aa15c 100644 --- a/concordium-node/Cargo.toml +++ b/concordium-node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "concordium_node" -version = "10.0.9" # must be kept in sync with 'is_compatible_version' in 'src/configuration.rs' +version = "11.2.2" # must be kept in sync with 'is_compatible_version' in 'src/configuration.rs' description = "Concordium Node" authors = ["Concordium "] exclude = [".gitignore", ".gitlab-ci.yml", "test/**/*","**/**/.gitignore","**/**/.gitlab-ci.yml"] diff --git a/concordium-node/README.md b/concordium-node/README.md index 6c299c7cd1..405c83c9e5 100644 --- a/concordium-node/README.md +++ b/concordium-node/README.md @@ -2,7 +2,7 @@ ## Dependencies to build the project -- Rust (stable 1.82 for using static libraries) +- The Rust toolchain specified in [`rust-toolchain.toml`](../rust-toolchain.toml) - binutils >= 2.22 - For macOS one should use the binutils provided by Xcode. - cmake >= 3.8.0 @@ -10,7 +10,7 @@ v22.12.06 is what we currently use. Either build from the v22.12.06 tag of the repository using CMake and copy the `flatc` binary somewhere in your PATH, or download a released binary from and place it somewhere in your PATH. - protobuf >= 3.15 - LLVM and Clang >= 3.9 -- As noted in the [caveats](#caveats) section below, you need to build the [Concordium Consensus](../concordium-consensus/) project before building this project. +- As noted in the [Building the node](#building-the-node) section below, you need to build the [Concordium Consensus](../concordium-consensus/) project before building this project. So you also need the [dependencies listed in the README for Concordium Consensus](https://github.com/Concordium/concordium-node/tree/main/concordium-consensus#build-requirements). ### Optional dependencies diff --git a/concordium-node/benches/p2p_lib_benchmark.rs b/concordium-node/benches/p2p_lib_benchmark.rs index f7921ba17e..497582e035 100644 --- a/concordium-node/benches/p2p_lib_benchmark.rs +++ b/concordium-node/benches/p2p_lib_benchmark.rs @@ -1,11 +1,6 @@ #[macro_use] extern crate criterion; -mod nop { - use criterion::Criterion; - pub fn nop_bench(_c: &mut Criterion) {} -} - macro_rules! bench_s11n { ($name:expr) => { use concordium_node::{network::NetworkMessage, test_utils::create_random_packet}; @@ -41,71 +36,6 @@ macro_rules! bench_s11n { }; } -#[cfg(feature = "dedup_benchmarks")] -macro_rules! dedup_bench { - ($f:ident, $hasher:ty, $hasher_name:expr, $hash_size:expr, $msg_size:expr) => { - pub fn $f(c: &mut Criterion) { - const MSG_SIZE: usize = $msg_size; - let mut group = c.benchmark_group(format!( - "{} dedup queue with {} B messages", - $hasher_name, $msg_size - )); - for &size in &[1024, 4096, 1024 * 16, 1024 * 32] { - let mut queue = CircularQueue::with_capacity(size); - for _ in 0..size { - let mut msg_hash = [0u8; $hash_size]; - msg_hash.copy_from_slice(&<$hasher>::digest(&generate_random_data(MSG_SIZE))); - queue.push(msg_hash); - } - - if MSG_SIZE > 4_000_000 { - group.measurement_time(Duration::from_secs(240)); - } else if MSG_SIZE > 1_000_000 { - group.measurement_time(Duration::from_secs(60)); - } - - group.throughput(Throughput::Elements(size as u64)); - group.bench_function(BenchmarkId::from_parameter(size), |b| { - b.iter(|| { - let new_msg = generate_random_data($msg_size); - let mut new_msg_hash = [0u8; $hash_size]; - new_msg_hash.copy_from_slice(&<$hasher>::digest(&new_msg)); - - if !queue.iter().any(|h| h == &new_msg_hash) { - queue.push(new_msg_hash); - } - }) - }); - } - group.finish(); - } - }; -} - -#[cfg(feature = "dedup_benchmarks")] -mod dedup { - use circular_queue::CircularQueue; - use concordium_node::test_utils::generate_random_data; - use criterion::{BenchmarkId, Criterion, Throughput}; - use digest::Digest; - use sha2::Sha256; - use std::time::Duration; - use twox_hash::XxHash64; - - dedup_bench!(small_bench_dedup_xxhash64, XxHash64, "XxHash64", 8, 250); - dedup_bench!(small_bench_dedup_sha256, Sha256, "SHA256", 32, 250); - dedup_bench!( - medium_bench_dedup_xxhash64, - XxHash64, - "XxHash64", - 8, - 1_048_576 - ); - dedup_bench!(medium_bench_dedup_sha256, Sha256, "SHA256", 32, 1_048_576); - dedup_bench!(big_bench_dedup_xxhash64, XxHash64, "XxHash64", 8, 4_194_304); - dedup_bench!(big_bench_dedup_sha256, Sha256, "SHA256", 32, 4_194_304); -} - mod s11n { pub mod fbs { bench_s11n!("flatbuffers"); @@ -114,17 +44,4 @@ mod s11n { criterion_group!(s11n_fbs_benches, s11n::fbs::bench_s11n); -#[cfg(feature = "dedup_benchmarks")] -criterion_group!( - dedup_benches, - dedup::small_bench_dedup_xxhash64, - dedup::small_bench_dedup_sha256, - dedup::medium_bench_dedup_xxhash64, - dedup::medium_bench_dedup_sha256, - dedup::big_bench_dedup_xxhash64, - dedup::big_bench_dedup_sha256 -); -#[cfg(not(feature = "dedup_benchmarks"))] -criterion_group!(dedup_benches, nop::nop_bench); - -criterion_main!(s11n_fbs_benches, dedup_benches,); +criterion_main!(s11n_fbs_benches); diff --git a/concordium-node/build.rs b/concordium-node/build.rs index d8b05ed2fa..6284fb6d6f 100644 --- a/concordium-node/build.rs +++ b/concordium-node/build.rs @@ -218,6 +218,24 @@ fn build_grpc2(proto_root_input: &str) -> std::io::Result<()> { .codec_path("crate::grpc2::RawCodec") .build(), ) + .method( + tonic_build::manual::Method::builder() + .name("get_token_authorizations") + .route_name("GetTokenAuthorizations") + .input_type("crate::grpc2::types::TokenAuthorizationsRequest") + .output_type("Vec") + .codec_path("crate::grpc2::RawCodec") + .build(), + ) + .method( + tonic_build::manual::Method::builder() + .name("get_lock_info") + .route_name("GetLockInfo") + .input_type("crate::grpc2::types::LockInfoRequest") + .output_type("Vec") + .codec_path("crate::grpc2::RawCodec") + .build(), + ) .method( tonic_build::manual::Method::builder() .name("get_account_list") @@ -238,6 +256,16 @@ fn build_grpc2(proto_root_input: &str) -> std::io::Result<()> { .server_streaming() .build(), ) + .method( + tonic_build::manual::Method::builder() + .name("get_lock_list") + .route_name("GetLockList") + .input_type("crate::grpc2::types::BlockHashInput") + .output_type("Vec") + .codec_path("crate::grpc2::RawCodec") + .server_streaming() + .build(), + ) .method( tonic_build::manual::Method::builder() .name("get_module_list") @@ -894,7 +922,8 @@ fn link_static_libs() -> std::io::Result<()> { out_dir ); println!("cargo:rustc-link-lib=static=Rcrypto"); - println!("cargo:rustc-link-lib=static=concordium_smart_contract_engine"); + + println!("cargo:rustc-link-lib=static=node_rust_library"); println!("cargo:rustc-link-lib=dylib=gmp"); diff --git a/concordium-node/src/consensus_ffi/ffi.rs b/concordium-node/src/consensus_ffi/ffi.rs index 650040ca77..b69e000912 100644 --- a/concordium-node/src/consensus_ffi/ffi.rs +++ b/concordium-node/src/consensus_ffi/ffi.rs @@ -540,7 +540,7 @@ extern "C" { copier: CopyToVecCallback, ) -> i64; - /// Get information about a specific account in a given block. + /// Get information about a specific token in a given block. /// /// * `consensus` - Pointer to the current consensus. /// * `block_id_type` - Type of block identifier. @@ -550,7 +550,7 @@ extern "C" { /// * `token_id_len` - Length of the token identifier. /// * `out_hash` - Location to write the block hash used in the query. /// * `out` - Location to write the output of the query. - /// * `copier` - Callback for writting the output. + /// * `copier` - Callback for writing the output. pub fn getTokenInfoV2( consensus: *mut consensus_runner, block_id_type: u8, @@ -562,6 +562,73 @@ extern "C" { copier: CopyToVecCallback, ) -> i64; + /// Get information about the token authorizations in a given block. + /// + /// * `consensus` - Pointer to the current consensus. + /// * `block_id_type` - Type of block identifier. + /// * `block_id` - Location with the block identifier. Length must match the + /// corresponding type of block identifier. + /// * `token_id` - Pointer to the token identifier. + /// * `token_id_len` - Length of the token identifier. + /// * `out_hash` - Location to write the block hash used in the query. + /// * `out` - Location to write the output of the query. + /// * `copier` - Callback for writting the output. + pub fn getTokenAuthorizationsV2( + consensus: *mut consensus_runner, + block_id_type: u8, + block_id: *const u8, + token_id: *const u8, + token_id_len: u8, + out_hash: *mut u8, + out: *mut Vec, + copier: CopyToVecCallback, + ) -> i64; + + /// Stream the list of all PLT Lock IDs that exist in the given block. + /// + /// Individual Lock IDs are enqueued using the provided callback. + /// + /// * `consensus` - Pointer to the current consensus. + /// * `stream` - Pointer to the response stream. + /// * `block_id_type` - Type of block identifier. + /// * `block_id` - Location with the block identifier. Length must match the + /// corresponding type of block identifier. + /// * `out_hash` - Location to write the block hash used in the query. + /// * `callback` - Callback for writing to the response stream. + pub fn getLockListV2( + consensus: *mut consensus_runner, + stream: *mut futures::channel::mpsc::Sender, tonic::Status>>, + block_id_type: u8, + block_id: *const u8, + out_hash: *mut u8, + callback: extern "C" fn( + *mut futures::channel::mpsc::Sender, tonic::Status>>, + *const u8, + i64, + ) -> i32, + ) -> i64; + + /// Get the proto-encoded `LockInfo` message for a single lock in a given block. + /// + /// * `consensus` - Pointer to the current consensus. + /// * `block_id_type` - Type of block identifier. + /// * `block_id` - Location with the block identifier. Length must match the + /// corresponding type of block identifier. + /// * `lock_id` - Pointer to 24 bytes containing the serialized `LockId` (three + /// big-endian `u64` fields: `account_index`, `sequence_number`, `creation_order`). + /// * `out_hash` - Location to write the block hash used in the query. + /// * `out` - Location to write the proto-encoded `plt.LockInfo` bytes. + /// * `copier` - Callback for writing the output. + pub fn getLockInfoV2( + consensus: *mut consensus_runner, + block_id_type: u8, + block_id: *const u8, + lock_id: *const [u8; 24], + out_hash: *mut u8, + out: *mut Vec, + copier: CopyToVecCallback, + ) -> i64; + /// Get next account sequence number. /// /// * `consensus` - Pointer to the current consensus. @@ -2413,6 +2480,48 @@ impl ConsensusContainer { Ok((out_hash, out_data)) } + /// Get the authorizations for a protocol-level token in a block, introduced as part of P11. + /// The return value is a pair of the block hash which was used for the + /// query, and the protobuf serialized response. + /// + /// If the token cannot be found then a [tonic::Status::not_found] is + /// returned. + pub fn get_token_authorizations_v2( + &self, + block_hash: &crate::grpc2::types::BlockHashInput, + token_id: &crate::grpc2::types::plt::TokenId, + ) -> Result<([u8; 32], Vec), tonic::Status> { + 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.len(); + if token_id_len > 255 { + return Err(tonic::Status::invalid_argument( + "TokenId: length must be at most 255 bytes", + )); + } + let token_id_len = token_id_len as u8; + let token_id_ptr = token_id.value.as_ptr(); + let consensus = self.consensus.load(Ordering::SeqCst); + let mut out_data: Vec = Vec::new(); + let mut out_hash = [0u8; 32]; + let response: ConsensusQueryResponse = unsafe { + getTokenAuthorizationsV2( + consensus, + block_id_type, + block_hash.as_ptr(), + token_id_ptr, + token_id_len, + out_hash.as_mut_ptr(), + &mut out_data, + copy_to_vec_callback, + ) + .try_into()? + }; + response.ensure_ok("tokenId or block")?; + Ok((out_hash, out_data)) + } + /// Get the best guess as to what the next account sequence number should /// be. If all account transactions are finalized, then this information /// is reliable. Otherwise, this is the best guess, assuming all other @@ -2584,6 +2693,85 @@ impl ConsensusContainer { } } + /// Look up locks in the given block, and return a stream of their + /// `LockId`s. + /// + /// Mirrors [`Self::get_token_list_v2`]. The return value is a block hash + /// used for the query. + /// + /// If the requested block does not exist a [tonic::Status::not_found] is returned. + pub fn get_lock_list_v2( + &self, + block_hash: &crate::grpc2::types::BlockHashInput, + sender: futures::channel::mpsc::Sender, tonic::Status>>, + ) -> Result<[u8; 32], tonic::Status> { + use crate::grpc2::Require; + + let sender = Box::new(sender); + let consensus = self.consensus.load(Ordering::SeqCst); + let mut buf = [0u8; 32]; + let bhi = crate::grpc2::types::block_hash_input_to_ffi(block_hash).require()?; + let (block_id_type, block_hash) = bhi.to_ptr(); + let sender_ptr = Box::into_raw(sender); + + let response: ConsensusQueryResponse = unsafe { + getLockListV2( + consensus, + sender_ptr, + block_id_type, + block_hash.as_ptr(), + buf.as_mut_ptr(), + enqueue_bytearray_callback, + ) + } + .try_into()?; + + if let Err(e) = response.ensure_ok("block") { + let _ = unsafe { Box::from_raw(sender_ptr) }; // deallocate sender since it is unused by Haskell. + Err(e) + } else { + Ok(buf) + } + } + + /// Get the `plt.LockInfo` proto-encoded payload for a single lock in a given + /// block. The Haskell side performs the proto encoding; this method returns the + /// wire bytes for the gRPC handler to forward through `RawCodec`. + /// + /// If the lock cannot be found a [tonic::Status::not_found] is returned. + pub fn get_lock_info_v2( + &self, + block_hash: &crate::grpc2::types::BlockHashInput, + lock_id: &crate::grpc2::types::plt::LockId, + ) -> Result<([u8; 32], Vec), tonic::Status> { + 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 lock_id = crate::grpc2::types::lock_id_to_ffi(lock_id); + + let consensus = self.consensus.load(Ordering::SeqCst); + let mut out_data: Vec = Vec::new(); + let mut out_hash = [0u8; 32]; + + let response: ConsensusQueryResponse = unsafe { + getLockInfoV2( + consensus, + block_id_type, + block_hash.as_ptr(), + &lock_id, + out_hash.as_mut_ptr(), + &mut out_data, + copy_to_vec_callback, + ) + .try_into()? + }; + + response.ensure_ok("lockId or block")?; + Ok((out_hash, out_data)) + } + /// Get a list of all smart contract modules. The stream will end /// when all modules that exist in the state at the end of the given /// block have been returned. diff --git a/concordium-node/src/grpc2.rs b/concordium-node/src/grpc2.rs index 6af15609b9..1f252cd4f1 100644 --- a/concordium-node/src/grpc2.rs +++ b/concordium-node/src/grpc2.rs @@ -229,6 +229,17 @@ pub mod types { } } + // Serialize the LockId as three big-endian u64 fields, exactly 24 bytes. + // This matches the Haskell `SerializedLockId` type and the FFI input contract + // documented on `getLockInfoV2`. + pub(crate) fn lock_id_to_ffi(lock_id: &plt::LockId) -> [u8; 24] { + let mut lock_id_bytes = [0u8; 24]; + lock_id_bytes[0..8].copy_from_slice(&lock_id.account_index.to_be_bytes()); + lock_id_bytes[8..16].copy_from_slice(&lock_id.sequence_number.to_be_bytes()); + lock_id_bytes[16..24].copy_from_slice(&lock_id.creation_order.to_be_bytes()); + lock_id_bytes + } + impl From for concordium_base::common::types::Amount { fn from(n: Amount) -> Self { Self::from_micro_ccd(n.value) @@ -919,10 +930,16 @@ struct ServiceConfig { #[serde(default)] get_token_list: bool, #[serde(default)] + get_lock_list: bool, + #[serde(default)] get_account_info: bool, #[serde(default)] get_token_info: bool, #[serde(default)] + get_token_authorizations: bool, + #[serde(default)] + get_lock_info: bool, + #[serde(default)] get_module_list: bool, #[serde(default)] get_module_source: bool, @@ -1048,8 +1065,11 @@ impl ServiceConfig { get_blocks: true, get_account_list: true, get_token_list: true, + get_lock_list: true, get_account_info: true, get_token_info: true, + get_token_authorizations: true, + get_lock_info: true, get_module_list: true, get_module_source: true, get_instance_list: true, @@ -1215,7 +1235,7 @@ pub mod server { }, messaging::{ConsensusMessage, MessageType}, }, - health, + grpc2, health, p2p::P2PNode, }; use anyhow::Context; @@ -1729,13 +1749,15 @@ pub mod server { futures::channel::mpsc::Receiver, tonic::Status>>; /// Return type for the 'GetTokenList' method. type GetTokenListStream = futures::channel::mpsc::Receiver, tonic::Status>>; + /// Return type for the 'GetLockList' method. + type GetLockListStream = futures::channel::mpsc::Receiver, tonic::Status>>; /// Return type for the 'GetWinningBakersEpoch' method. type GetWinningBakersEpochStream = futures::channel::mpsc::Receiver, tonic::Status>>; async fn get_blocks( &self, - _request: tonic::Request, + _request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_blocks { return Err(tonic::Status::unimplemented("`GetBlocks` is not enabled.")); @@ -1757,7 +1779,7 @@ pub mod server { async fn get_finalized_blocks( &self, - _request: tonic::Request, + _request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_finalized_blocks { return Err(tonic::Status::unimplemented( @@ -1781,7 +1803,7 @@ pub mod server { async fn get_account_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_account_info { return Err(tonic::Status::unimplemented( @@ -1804,7 +1826,7 @@ pub mod server { async fn get_token_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_token_info { return Err(tonic::Status::unimplemented( @@ -1825,9 +1847,32 @@ pub mod server { Ok(response) } + async fn get_token_authorizations( + &self, + request: tonic::Request, + ) -> Result>, tonic::Status> { + if !self.service_config.get_token_authorizations { + return Err(tonic::Status::unimplemented( + "`GetTokenAuthorizations` is not enabled.", + )); + } + let (hash, response) = self + .run_blocking(move |consensus| { + let request = request.get_ref(); + let block_hash = request.block_hash.as_ref().require()?; + let token_identifier = request.token_id.as_ref().require()?; + consensus.get_token_authorizations_v2(block_hash, token_identifier) + }) + .await?; + + let mut response = tonic::Response::new(response); + add_hash(&mut response, hash)?; + Ok(response) + } + async fn get_account_list( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_account_list { return Err(tonic::Status::unimplemented( @@ -1847,7 +1892,7 @@ pub mod server { async fn get_token_list( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_token_list { return Err(tonic::Status::unimplemented( @@ -1865,9 +1910,52 @@ pub mod server { Ok(response) } + async fn get_lock_list( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + if !self.service_config.get_lock_list { + return Err(tonic::Status::unimplemented( + "`GetLockList` is not enabled.", + )); + } + let (sender, receiver) = futures::channel::mpsc::channel(100); + let hash = self + .run_blocking(move |consensus| { + consensus.get_lock_list_v2(request.get_ref(), sender) + }) + .await?; + let mut response = tonic::Response::new(receiver); + add_hash(&mut response, hash)?; + Ok(response) + } + + async fn get_lock_info( + &self, + request: tonic::Request, + ) -> Result>, tonic::Status> { + if !self.service_config.get_lock_info { + return Err(tonic::Status::unimplemented( + "`GetLockInfo` is not enabled.", + )); + } + let (hash, response) = self + .run_blocking(move |consensus| { + let request = request.get_ref(); + let block_hash = request.block_hash.as_ref().require()?; + let lock_id = request.lock_id.as_ref().require()?; + consensus.get_lock_info_v2(block_hash, lock_id) + }) + .await?; + + let mut response = tonic::Response::new(response); + add_hash(&mut response, hash)?; + Ok(response) + } + async fn get_module_list( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_module_list { return Err(tonic::Status::unimplemented( @@ -1887,7 +1975,7 @@ pub mod server { async fn get_module_source( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_module_source { return Err(tonic::Status::unimplemented( @@ -1909,7 +1997,7 @@ pub mod server { async fn get_instance_list( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_instance_list { return Err(tonic::Status::unimplemented( @@ -1929,7 +2017,7 @@ pub mod server { async fn get_instance_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_instance_info { return Err(tonic::Status::unimplemented( @@ -1951,7 +2039,7 @@ pub mod server { async fn get_instance_state( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_instance_state { return Err(tonic::Status::unimplemented( @@ -2053,7 +2141,7 @@ pub mod server { async fn get_next_account_sequence_number( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_next_account_sequence_number { return Err(tonic::Status::unimplemented( @@ -2070,7 +2158,7 @@ pub mod server { async fn get_consensus_info( &self, - _request: tonic::Request, + _request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_consensus_info { return Err(tonic::Status::unimplemented( @@ -2085,7 +2173,7 @@ pub mod server { async fn get_consensus_detailed_status( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_consensus_detailed_status { return Err(tonic::Status::unimplemented( @@ -2103,7 +2191,7 @@ pub mod server { async fn get_ancestors( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_ancestors { return Err(tonic::Status::unimplemented( @@ -2126,7 +2214,7 @@ pub mod server { async fn get_block_item_status( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_block_item_status { return Err(tonic::Status::unimplemented( @@ -2143,7 +2231,7 @@ pub mod server { async fn invoke_instance( &self, - mut request: tonic::Request, + mut request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.invoke_instance { return Err(tonic::Status::unimplemented( @@ -2154,7 +2242,7 @@ pub mod server { if let Some(nrg) = request.get_ref().energy.as_ref() { max_energy = std::cmp::min(max_energy, nrg.value); } - request.get_mut().energy = Some(crate::grpc2::types::Energy { value: max_energy }); + request.get_mut().energy = Some(grpc2::types::Energy { value: max_energy }); let (hash, response) = self .run_blocking(move |consensus| consensus.invoke_instance_v2(request.get_ref())) .await?; @@ -2165,9 +2253,8 @@ pub mod server { async fn get_cryptographic_parameters( &self, - request: tonic::Request, - ) -> Result, tonic::Status> - { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.get_cryptographic_parameters { return Err(tonic::Status::unimplemented( "`GetCryptographicParameters` is not enabled.", @@ -2185,7 +2272,7 @@ pub mod server { async fn get_block_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_block_info { return Err(tonic::Status::unimplemented( @@ -2202,7 +2289,7 @@ pub mod server { async fn get_baker_list( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_baker_list { return Err(tonic::Status::unimplemented( @@ -2222,7 +2309,7 @@ pub mod server { async fn get_pool_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_pool_info { return Err(tonic::Status::unimplemented( @@ -2239,7 +2326,7 @@ pub mod server { async fn get_passive_delegation_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_passive_delegation_info { return Err(tonic::Status::unimplemented( @@ -2258,7 +2345,7 @@ pub mod server { async fn get_blocks_at_height( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_blocks_at_height { return Err(tonic::Status::unimplemented( @@ -2274,7 +2361,7 @@ pub mod server { async fn get_tokenomics_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_tokenomics_info { return Err(tonic::Status::unimplemented( @@ -2291,7 +2378,7 @@ pub mod server { async fn get_pool_delegators( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_pool_delegators { return Err(tonic::Status::unimplemented( @@ -2311,7 +2398,7 @@ pub mod server { async fn get_pool_delegators_reward_period( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_pool_delegators_reward_period { @@ -2332,7 +2419,7 @@ pub mod server { async fn get_passive_delegators( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_passive_delegators { return Err(tonic::Status::unimplemented( @@ -2352,7 +2439,7 @@ pub mod server { async fn get_passive_delegators_reward_period( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_passive_delegators_reward_period { @@ -2373,7 +2460,7 @@ pub mod server { async fn get_branches( &self, - _request: tonic::Request, + _request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_branches { return Err(tonic::Status::unimplemented( @@ -2388,7 +2475,7 @@ pub mod server { async fn get_election_info( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_election_info { return Err(tonic::Status::unimplemented( @@ -2405,7 +2492,7 @@ pub mod server { async fn get_identity_providers( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_identity_providers { return Err(tonic::Status::unimplemented( @@ -2425,7 +2512,7 @@ pub mod server { async fn get_anonymity_revokers( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_anonymity_revokers { return Err(tonic::Status::unimplemented( @@ -2445,7 +2532,7 @@ pub mod server { async fn get_account_non_finalized_transactions( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_account_non_finalized_transactions { @@ -2464,7 +2551,7 @@ pub mod server { async fn get_block_transaction_events( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_block_transaction_events { return Err(tonic::Status::unimplemented( @@ -2484,7 +2571,7 @@ pub mod server { async fn get_block_special_events( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_block_special_events { return Err(tonic::Status::unimplemented( @@ -2504,7 +2591,7 @@ pub mod server { async fn get_block_pending_updates( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_block_pending_updates { return Err(tonic::Status::unimplemented( @@ -2524,7 +2611,7 @@ pub mod server { async fn get_next_update_sequence_numbers( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_next_update_sequence_numbers { return Err(tonic::Status::unimplemented( @@ -2543,7 +2630,7 @@ pub mod server { async fn get_scheduled_release_accounts( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_scheduled_release_accounts { @@ -2564,7 +2651,7 @@ pub mod server { async fn get_cooldown_accounts( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_cooldown_accounts { return Err(tonic::Status::unimplemented( @@ -2584,7 +2671,7 @@ pub mod server { async fn get_pre_cooldown_accounts( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_pre_cooldown_accounts { return Err(tonic::Status::unimplemented( @@ -2604,7 +2691,7 @@ pub mod server { async fn get_pre_pre_cooldown_accounts( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_pre_pre_cooldown_accounts { return Err(tonic::Status::unimplemented( @@ -2624,7 +2711,7 @@ pub mod server { async fn get_block_chain_parameters( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_block_chain_parameters { return Err(tonic::Status::unimplemented( @@ -2643,7 +2730,7 @@ pub mod server { async fn get_block_finalization_summary( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_block_finalization_summary { return Err(tonic::Status::unimplemented( @@ -2662,7 +2749,7 @@ pub mod server { async fn get_bakers_reward_period( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_bakers_reward_period { return Err(tonic::Status::unimplemented( @@ -2682,7 +2769,7 @@ pub mod server { async fn get_baker_earliest_win_time( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_baker_earliest_win_time { return Err(tonic::Status::unimplemented( @@ -2700,13 +2787,13 @@ pub mod server { async fn shutdown( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.shutdown { return Err(tonic::Status::unimplemented("`Shutdown` is not enabled.")); } match self.node.close() { - Ok(_) => Ok(tonic::Response::new(crate::grpc2::types::Empty {})), + Ok(_) => Ok(tonic::Response::new(grpc2::types::Empty {})), Err(e) => Err(tonic::Status::internal(format!( "Unable to shutdown server {}.", e @@ -2716,8 +2803,8 @@ pub mod server { async fn peer_connect( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.peer_connect { return Err(tonic::Status::unimplemented( "`PeerConnect` is not enabled.", @@ -2737,7 +2824,7 @@ pub mod server { peer_type: crate::common::PeerType::Node, given: true, }); - Ok(tonic::Response::new(crate::grpc2::types::Empty {})) + Ok(tonic::Response::new(grpc2::types::Empty {})) } else { Err(tonic::Status::invalid_argument("Invalid IP address.")) } @@ -2746,8 +2833,8 @@ pub mod server { async fn peer_disconnect( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.peer_disconnect { return Err(tonic::Status::unimplemented( "`PeerDisconnect` is not enabled.", @@ -2762,7 +2849,7 @@ pub mod server { if let Ok(ip) = peer_connect.ip.require()?.value.parse::() { let addr = SocketAddr::new(ip, peer_connect.port.require()?.value as u16); if self.node.drop_addr(addr) { - Ok(tonic::Response::new(crate::grpc2::types::Empty {})) + Ok(tonic::Response::new(grpc2::types::Empty {})) } else { Err(tonic::Status::not_found("The peer was not found.")) } @@ -2774,8 +2861,8 @@ pub mod server { async fn get_banned_peers( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.get_banned_peers { return Err(tonic::Status::unimplemented( "`GetBannedPeers` is not enabled.", @@ -2788,14 +2875,12 @@ pub mod server { let ip_address = match banned_peer { crate::p2p::bans::PersistedBanId::Ip(addr) => addr.to_string(), }; - crate::grpc2::types::BannedPeer { - ip_address: Some(crate::grpc2::types::IpAddress { value: ip_address }), + grpc2::types::BannedPeer { + ip_address: Some(grpc2::types::IpAddress { value: ip_address }), } }) .collect(); - Ok(tonic::Response::new(crate::grpc2::types::BannedPeers { - peers, - })) + Ok(tonic::Response::new(grpc2::types::BannedPeers { peers })) } else { Err(tonic::Status::internal("Could not load banned peers.")) } @@ -2803,15 +2888,15 @@ pub mod server { async fn ban_peer( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.ban_peer { return Err(tonic::Status::unimplemented("`BanPeer` is not enabled.")); } let ip = request.into_inner().ip_address.require()?; match ip.value.parse::() { Ok(ip_addr) => match self.node.drop_by_ip_and_ban(ip_addr) { - Ok(_) => Ok(tonic::Response::new(crate::grpc2::types::Empty {})), + Ok(_) => Ok(tonic::Response::new(grpc2::types::Empty {})), Err(e) => Err(tonic::Status::internal(format!( "Could not ban peer {}.", e @@ -2826,8 +2911,8 @@ pub mod server { async fn unban_peer( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.unban_peer { return Err(tonic::Status::unimplemented("`UnbanPeer` is not enabled.")); } @@ -2841,7 +2926,7 @@ pub mod server { Ok(ip_addr) => { let banned_id = crate::p2p::bans::PersistedBanId::Ip(ip_addr); match self.node.unban_node(banned_id) { - Ok(_) => Ok(tonic::Response::new(crate::grpc2::types::Empty {})), + Ok(_) => Ok(tonic::Response::new(grpc2::types::Empty {})), Err(e) => Err(tonic::Status::internal(format!( "Could not unban peer {}.", e @@ -2858,8 +2943,8 @@ pub mod server { #[cfg(feature = "network_dump")] async fn dump_start( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.dump_start { return Err(tonic::Status::unimplemented("`DumpStart` is not enabled.")); } @@ -2870,7 +2955,7 @@ pub mod server { )) } else { match self.node.activate_dump(&file_path, request.get_ref().raw) { - Ok(_) => Ok(tonic::Response::new(crate::grpc2::types::Empty {})), + Ok(_) => Ok(tonic::Response::new(grpc2::types::Empty {})), Err(e) => Err(tonic::Status::internal(format!( "Could not start network dump {}", e @@ -2882,8 +2967,8 @@ pub mod server { #[cfg(not(feature = "network_dump"))] async fn dump_start( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.dump_start { return Err(tonic::Status::unimplemented("`DumpStart` is not enabled.")); } @@ -2895,13 +2980,13 @@ pub mod server { #[cfg(feature = "network_dump")] async fn dump_stop( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.dump_stop { return Err(tonic::Status::unimplemented("`DumpStop` is not enabled.")); } match self.node.stop_dump() { - Ok(_) => Ok(tonic::Response::new(crate::grpc2::types::Empty {})), + Ok(_) => Ok(tonic::Response::new(grpc2::types::Empty {})), Err(e) => Err(tonic::Status::internal(format!( "Could not stop network dump {}", e @@ -2912,8 +2997,8 @@ pub mod server { #[cfg(not(feature = "network_dump"))] async fn dump_stop( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.dump_stop { return Err(tonic::Status::unimplemented("`DumpStop` is not enabled.")); } @@ -2924,8 +3009,8 @@ pub mod server { async fn get_peers_info( &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { + _request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.get_peers_info { return Err(tonic::Status::unimplemented( "`GetPeersInfo` is not enabled.", @@ -2939,7 +3024,7 @@ pub mod server { .into_iter() .map(|peer_stats| { // Collect the network statistics - let network_stats = Some(crate::grpc2::types::peers_info::peer::NetworkStats { + let network_stats = Some(grpc2::types::peers_info::peer::NetworkStats { packets_sent: peer_stats.msgs_sent, packets_received: peer_stats.msgs_received, latency: peer_stats.latency, @@ -2950,39 +3035,39 @@ pub mod server { crate::common::PeerType::Node => { let catchup_status = match peer_statuses.get(&peer_stats.local_id) { Some(crate::consensus_ffi::catch_up::PeerStatus::CatchingUp) => { - crate::grpc2::types::peers_info::peer::CatchupStatus::Catchingup + grpc2::types::peers_info::peer::CatchupStatus::Catchingup } Some(crate::consensus_ffi::catch_up::PeerStatus::UpToDate) => { - crate::grpc2::types::peers_info::peer::CatchupStatus::Uptodate + grpc2::types::peers_info::peer::CatchupStatus::Uptodate } - _ => crate::grpc2::types::peers_info::peer::CatchupStatus::Pending, + _ => grpc2::types::peers_info::peer::CatchupStatus::Pending, }; - crate::grpc2::types::peers_info::peer::ConsensusInfo::NodeCatchupStatus( + grpc2::types::peers_info::peer::ConsensusInfo::NodeCatchupStatus( catchup_status.into(), ) } // Bootstrappers do not have a catchup status as they are not participating // in the consensus protocol. crate::common::PeerType::Bootstrapper => { - crate::grpc2::types::peers_info::peer::ConsensusInfo::Bootstrapper( - crate::grpc2::types::Empty::default(), + grpc2::types::peers_info::peer::ConsensusInfo::Bootstrapper( + grpc2::types::Empty::default(), ) } }; // Get the catchup status of the peer. - let socket_address = crate::grpc2::types::IpSocketAddress { - ip: Some(crate::grpc2::types::IpAddress { + let socket_address = grpc2::types::IpSocketAddress { + ip: Some(grpc2::types::IpAddress { value: peer_stats.external_address().ip().to_string(), }), - port: Some(crate::grpc2::types::Port { + port: Some(grpc2::types::Port { value: peer_stats.external_port as u32, }), }; // Wrap the peer id. - let peer_id = crate::grpc2::types::PeerId { + let peer_id = grpc2::types::PeerId { value: format!("{}", peer_stats.self_id), }; - crate::grpc2::types::peers_info::Peer { + grpc2::types::peers_info::Peer { peer_id: Some(peer_id), socket_address: Some(socket_address), consensus_info: Some(consensus_info), @@ -2990,14 +3075,12 @@ pub mod server { } }) .collect(); - Ok(tonic::Response::new(crate::grpc2::types::PeersInfo { - peers, - })) + Ok(tonic::Response::new(grpc2::types::PeersInfo { peers })) } async fn get_node_info( &self, - _request: tonic::Request, + _request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_node_info { return Err(tonic::Status::unimplemented( @@ -3123,8 +3206,8 @@ pub mod server { async fn send_block_item( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { use ConsensusFfiResponse::*; if !self.service_config.send_block_item { return Err(tonic::Status::unimplemented( @@ -3186,7 +3269,7 @@ pub mod server { )); } }; - Ok(tonic::Response::new(crate::grpc2::types::TransactionHash { + Ok(tonic::Response::new(grpc2::types::TransactionHash { value: transaction_hash.to_vec(), })) } @@ -3223,8 +3306,8 @@ pub mod server { async fn get_account_transaction_sign_hash( &self, - request: tonic::Request, - ) -> Result, tonic::Status> + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.get_account_transaction_sign_hash { return Err(tonic::Status::unimplemented( @@ -3242,7 +3325,7 @@ pub mod server { async fn get_block_items( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_block_items { return Err(tonic::Status::unimplemented( @@ -3262,7 +3345,7 @@ pub mod server { async fn get_block_certificates( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result>, tonic::Status> { if !self.service_config.get_block_certificates { return Err(tonic::Status::unimplemented( @@ -3281,8 +3364,8 @@ pub mod server { async fn get_first_block_epoch( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { if !self.service_config.get_first_block_epoch { return Err(tonic::Status::unimplemented( "`GetFirstBlockEpoch` is not enabled.", @@ -3300,7 +3383,7 @@ pub mod server { async fn get_winning_bakers_epoch( &self, - request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { if !self.service_config.get_winning_bakers_epoch { return Err(tonic::Status::unimplemented( @@ -3317,7 +3400,7 @@ pub mod server { async fn dry_run( &self, - request: tonic::Request>, + request: tonic::Request>, ) -> Result, tonic::Status> { if !self.service_config.dry_run { return Err(tonic::Status::unimplemented("`DryRun` is not enabled.")); @@ -3441,9 +3524,9 @@ pub mod server { } } -#[expect(clippy::result_large_err)] /// Add a block hash to the metadata of a response. Used for returning the block /// hash. +#[expect(clippy::result_large_err)] fn add_hash(response: &mut tonic::Response, hash: [u8; 32]) -> Result<(), tonic::Status> { let value = tonic::metadata::MetadataValue::try_from(hex::encode(hash)) .map_err(|_| tonic::Status::internal("Cannot add metadata hash."))?; diff --git a/concordium-node/src/plugins/consensus.rs b/concordium-node/src/plugins/consensus.rs index a1a42a26c0..14ff669780 100644 --- a/concordium-node/src/plugins/consensus.rs +++ b/concordium-node/src/plugins/consensus.rs @@ -613,7 +613,7 @@ fn update_peer_states( } } else if [Block, FinalizationRecord, FinalizationMessage].contains(&request.variant) { match request.distribution_mode() { - DistributionMode::Direct if consensus_result.is_successful() => { + DistributionMode::Direct if consensus_result.is_rebroadcastable(request.variant) => { // Directly sent blocks, finalization records and finalization messages that are // successful (i.e. new and not pending) have special // handling for the purposes of catch-up. diff --git a/concordium-node/src/test_utils.rs b/concordium-node/src/test_utils.rs index ef0c92503d..39d10ffdb7 100644 --- a/concordium-node/src/test_utils.rs +++ b/concordium-node/src/test_utils.rs @@ -37,7 +37,7 @@ pub fn next_available_port() -> u16 { while available_port.is_none() { let port = PORT_OFFSET.fetch_add(1, Ordering::SeqCst) as u16 + PORT_START_NODE; available_port = TcpListener::bind(("127.0.0.1", port)).map(|_| port).ok(); - assert!(port < std::u16::MAX); + assert!(port < u16::MAX); } available_port.unwrap() diff --git a/macos_logger_wrapper/Cargo.lock b/macos_logger_wrapper/Cargo.lock new file mode 100644 index 0000000000..ab4c7ee114 --- /dev/null +++ b/macos_logger_wrapper/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macos_logger_wrapper" +version = "0.1.0" +dependencies = [ + "cc", + "log", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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/README.md b/plt-deployment-unit/README.md deleted file mode 100644 index 1c4895cc66..0000000000 --- a/plt-deployment-unit/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# concordium-node: Deployment unit 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/deployment-build-test.yaml/badge.svg) - -This project implements the runtime upgradable PLT deployment unit introduced as part of Concordium Protocol Version 10. - -The PLT deployment unit is written as a Rust library which is then compiled to [WebAssembly](https://webassembly.org/) and can be deployed on the Concordium blockchain by the Governance Committee. 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 64% rename from plt-deployment-unit/Cargo.lock rename to plt/Cargo.lock index 851be042c4..9daf160d52 100644 --- a/plt-deployment-unit/Cargo.lock +++ b/plt/Cargo.lock @@ -8,7 +8,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "version_check", ] @@ -25,6 +25,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -34,6 +40,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.100" @@ -155,7 +167,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", ] [[package]] @@ -164,6 +176,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 +202,39 @@ 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 = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] [[package]] name = "bitvec" @@ -211,9 +259,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 +269,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-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -244,9 +292,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 +326,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", @@ -304,6 +352,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.42" @@ -334,6 +393,32 @@ dependencies = [ "half", ] +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", + "terminal_size", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "concordium-contracts-common" version = "9.2.0" @@ -360,7 +445,40 @@ version = "4.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", +] + +[[package]] +name = "concordium-smart-contract-engine" +version = "6.1.0" +dependencies = [ + "anyhow", + "byteorder", + "concordium-contracts-common", + "concordium-wasm", + "derive_more", + "ed25519-zebra", + "libc", + "num_enum", + "rand 0.8.5", + "secp256k1", + "serde", + "sha2", + "sha3", + "slab", + "thiserror 1.0.69", + "tinyvec", +] + +[[package]] +name = "concordium-wasm" +version = "5.1.0" +dependencies = [ + "anyhow", + "concordium-contracts-common", + "derive_more", + "leb128", + "num_enum", ] [[package]] @@ -394,7 +512,7 @@ dependencies = [ "num", "num-bigint", "num-traits", - "rand", + "rand 0.8.5", "rayon", "rust_decimal", "serde", @@ -413,12 +531,18 @@ version = "1.2.0" dependencies = [ "convert_case 0.8.0", "darling 0.20.11", - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "const-oid" version = "0.9.6" @@ -455,6 +579,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -488,9 +621,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", @@ -503,12 +636,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", "group", - "rand_core", + "rand_core 0.6.4", "rustc_version", "subtle", "zeroize", @@ -522,7 +655,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -556,7 +689,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -570,7 +703,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -581,7 +714,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -592,7 +725,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -602,6 +735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -636,7 +770,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -649,6 +783,31 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "divan" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -662,6 +821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] @@ -673,7 +833,24 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0017d969298eec91e3db7a2985a8cab4df6341d86e6f3a6f5878b13fb7846bc9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.15.5", + "pkcs8", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -692,6 +869,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "ff" version = "0.13.1" @@ -699,7 +892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "bitvec", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -711,9 +904,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" @@ -721,6 +914,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "funty" version = "2.0.0" @@ -748,6 +947,32 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "group" version = "0.13.0" @@ -755,7 +980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -796,9 +1021,26 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -830,12 +1072,32 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -849,12 +1111,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", ] @@ -879,15 +1141,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", @@ -899,7 +1161,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -908,17 +1170,29 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[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" @@ -932,6 +1206,14 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "node-rust-library" +version = "0.1.0" +dependencies = [ + "concordium-smart-contract-engine", + "plt-scheduler", +] + [[package]] name = "nom" version = "7.1.3" @@ -1021,6 +1303,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1033,6 +1336,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1044,11 +1356,44 @@ dependencies = [ ] [[package]] -name = "plt-deployment-unit" +name = "plt-block-state" version = "0.1.0" dependencies = [ + "assert_matches", + "concordium-smart-contract-engine", "concordium_base", - "getrandom", + "divan", + "either", + "hex", + "im", + "libc", + "plt-scheduler-types", + "rand 0.10.0", + "sha2", + "thiserror 2.0.17", +] + +[[package]] +name = "plt-scheduler" +version = "0.1.0" +dependencies = [ + "assert_matches", + "concordium_base", + "hex", + "libc", + "plt-block-state", + "plt-scheduler-types", + "thiserror 2.0.17", +] + +[[package]] +name = "plt-scheduler-types" +version = "0.1.0" +dependencies = [ + "concordium_base", + "hex", + "proptest", + "thiserror 2.0.17", ] [[package]] @@ -1066,13 +1411,33 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -1084,6 +1449,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -1104,15 +1488,33 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[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", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -1126,8 +1528,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -1137,7 +1560,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1146,7 +1579,40 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", ] [[package]] @@ -1186,9 +1652,21 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rend" version = "0.4.2" @@ -1237,7 +1715,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -1252,17 +1730,42 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[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" @@ -1278,9 +1781,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", @@ -1294,6 +1797,24 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secp256k1" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1327,7 +1848,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1345,17 +1866,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", @@ -1364,14 +1885,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]] @@ -1381,7 +1902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1407,7 +1928,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1416,6 +1937,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "spki" version = "0.7.3" @@ -1451,9 +1988,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", @@ -1466,6 +2003,29 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1492,7 +2052,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1503,7 +2063,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1554,32 +2114,49 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", - "toml_datetime", + "indexmap 2.12.1", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[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", + "winnow 0.7.14", ] [[package]] @@ -1588,11 +2165,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[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" @@ -1600,11 +2183,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[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", @@ -1616,17 +2205,44 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[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", @@ -1637,9 +2253,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", @@ -1647,26 +2263,60 @@ 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", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.12.1", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.12.1", + "semver", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -1688,7 +2338,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1699,7 +2349,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.111", ] [[package]] @@ -1726,15 +2376,121 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[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", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.12.1", + "prettyplease", + "syn 2.0.111", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.12.1", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.12.1", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "wyz" version = "0.5.1" @@ -1746,22 +2502,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]] @@ -1781,5 +2537,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..c8d2388d2e --- /dev/null +++ b/plt/Cargo.toml @@ -0,0 +1,34 @@ +[workspace] +resolver = "3" + +members = [ + "node-rust-library", + "plt-block-state", + "plt-scheduler-types", + "plt-scheduler", +] + +# Optimize a good as possible with fat linking and a single code generation unit +[profile.release] +lto = "fat" +codegen-units = 1 + +[workspace.dependencies] +plt-block-state = {path = "plt-block-state"} +plt-lock-module = {path = "plt-lock-module"} +plt-scheduler = {path = "plt-scheduler"} +plt-scheduler-types = {path = "plt-scheduler-types"} + +concordium_base = {path = "../concordium-base/rust-src/concordium_base"} +concordium-smart-contract-engine = {path = "../concordium-base/smart-contracts/wasm-chain-integration"} + +assert_matches = "1.5.0" +divan = "0.1.21" +either = "1.15.0" +hex = "0.4.3" +im = "15.1.0" +libc = "0.2.178" +proptest = "1.9.0" +rand = "0.10.0" +sha2 = "0.10.9" +thiserror = "2.0.17" diff --git a/plt/README.md b/plt/README.md new file mode 100644 index 0000000000..8e717d1581 --- /dev/null +++ b/plt/README.md @@ -0,0 +1,19 @@ +# Protocol-level token (PLT) Scheduler + +[![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 project implements the scheduler (block item execution) for protocol-level tokens in Rust. +The present Rust implementation is currently used on P11 and upwards. The Haskell implementation is used on P9 and P10. +This may be changed, such that the Rust implementation is also used on P9 and P10. + +The PLT Scheduler is compiled as a native library and is used within the Haskell implemented scheduler found as part of [`concordium-consensus`](../concordium-consensus/README.md). +See [PLT Scheduler Component View](docs/component_view.md). + +The crates in the project are: +* `node-rust-library`: Crate build to a library for dynamic or static linking into the `concordium-concensus` Haskell project. +* `plt-scheduler-types`: Externally facing scheduler and query types. These types correspond to identical types on the Haskell side. +* `plt-scheduler-interface`: The internally facing scheduler interface seen by the token module, e.g. the token kernel. +* `plt-token-module`: The Token Module implementation. +* `plt-block-state`: The PLT block state implementation. +* `plt-scheduler`: The PLT Scheduler implementation. diff --git a/plt/docs/component_view.md b/plt/docs/component_view.md new file mode 100644 index 0000000000..c6ae25a723 --- /dev/null +++ b/plt/docs/component_view.md @@ -0,0 +1,94 @@ +# Rust PLT Scheduler Component View + +The diagram below depicts how the Rust PLT scheduler and the token module is embedded within the node and how it interacts with other components in the node. + +The Rust PLT scheduler is responsible for executing create PLT update instructions and PLT operations (transfer, mint/burn, pause/unpause, allow/deny lists). +The block item header (signatures, energy limit, etc.) is handled in the Haskell scheduler, only the block item payload is dispatched to the Rust PLT scheduler. +The PLT scheduler maintains circulating supply and balances. The token module maintains state related to allow/deny lists and pausing. The token module +only has access to state through the PLT kernel. + +Notice that in the current implementation, the Haskell scheduler maintains the token account state (balance) by request from the Rust PLT scheduler. For simplicity, this is +not depicted in the diagram. It may change in the future, such that the Rust PLT scheduler directly maintains this account level token state. + +```mermaid +C4Container + title Rust PLT scheduler component diagram + Container_Boundary(node, "Node (Haskell)") { + Component(consensus, "Consensus",, "Achieves consensus on blocks in the Concordium chain.") + Component(scheduler, "Scheduler",, "Executes block items. Checks block items headers (signature, fee charge)
and executes the block item payload. Is responsible for producing events,
reject reasons and updating block state.") + Component(queries, "Queries",, "Queries on block state.") + Component(block_state_comp, "BlockStateQuery/Operations (Haskell)",, "Queries on block state.") + + ComponentDb(tree_state, "Tree state (LMDB)",, "Block indexes.") + + Boundary(plt_scheduler_boundary, "PLT scheduler (Rust) (FFI)", "boundary") { + Component(plt_scheduler, "PLT Scheduler",, "Executes PLT block item bodies.
Maintains circulating supply and balances state.") + Component(plt_queries, "PLT Queries",, "Implements PLT queries on block state.") + Component(plt_block_state_comp, "BlockStateQuery/Operations (Rust)",, "Queries on block state.") + + Component(plt_kernel, "PLT Kernel",, "Executes PLT block item bodies.
Maintains circulating supply and balances state.") + + Boundary(token_module_boundary, "Token Module", "boundary") { + Component(token_module, "Token Module",, "Executes create PLT instruction and PLT operations
and maintains module state.") + Component(token_module_queries, "Token Module Queries",,"Implements queries on module state.") + } + } + + Boundary(block_state_boundary, "Block state (single flat file)", "boundary") { + ComponentDb(block_state, "Block state",, "State for each block.") + ComponentDb(plt_block_state, "PLT state",, "PLT state for each block.") + } + } + +Rel(consensus, tree_state, "Maintains tree state") +Rel(scheduler, block_state_comp, "Updates and queries block state") +Rel(block_state_comp, block_state, "Read/Write") +Rel(queries, block_state_comp, "Queries block state") +Rel(consensus, scheduler, "Execute block items") +Rel(queries, plt_queries, "Queries PLT state") +Rel(scheduler, plt_scheduler, "Executes PLT block item payloads") + +Rel(plt_block_state_comp, plt_block_state, "Read/Write") +Rel(plt_scheduler, plt_block_state_comp, "Updates and queries block state") +Rel(plt_kernel, plt_block_state_comp, "Updates and queries block state") +Rel(plt_queries, plt_block_state_comp, "Queries block state") +Rel(plt_scheduler, token_module, "Executes PLT operations
and module token initialization") +Rel(plt_queries, token_module_queries, "Queries module state") +Rel(plt_block_state_comp, block_state_comp, "Query accounts by address/index
Query and update account token balance") + +Rel(block_state, plt_block_state, "Opaque pointer") + +Rel(token_module, plt_kernel, "Updates and queries") +Rel(token_module_queries, plt_kernel, "Queries") + +UpdateElementStyle(consensus, $borderColor="black") +UpdateElementStyle(scheduler, $borderColor="black") +UpdateElementStyle(queries, $borderColor="black") +UpdateElementStyle(tree_state, $borderColor="black") + +UpdateElementStyle(plt_scheduler, $borderColor="black") +UpdateElementStyle(plt_queries, $borderColor="black") +UpdateElementStyle(plt_kernel, $borderColor="black") + +UpdateElementStyle(block_state, $borderColor="black") +UpdateElementStyle(plt_block_state, $borderColor="black") + +UpdateElementStyle(token_module, $borderColor="black") +UpdateElementStyle(token_module_queries, $borderColor="black") + +UpdateRelStyle(scheduler, block_state, $offsetX="-100") +UpdateRelStyle(scheduler, plt_scheduler, $lineColor="green", $textColor="green") +UpdateRelStyle(consensus, scheduler, $offsetX="-50") +UpdateRelStyle(queries, plt_queries, $lineColor="green", $textColor="green", $offsetY="-50", $offsetX="50") +UpdateRelStyle(scheduler, block_state, $offsetY="-50", $offsetX="-200") + +UpdateRelStyle(plt_scheduler, token_module, $offsetY="-150", $offsetX="-180") +UpdateRelStyle(plt_scheduler, plt_block_state_comp, $offsetY="-10", $offsetX="-250") +UpdateRelStyle(plt_kernel, plt_block_state_comp, $offsetY="0", $offsetX="-220") +UpdateRelStyle(plt_block_state_comp, block_state_comp, $lineColor="red", $textColor="red", $offsetY="-10", $offsetX="-200") + +UpdateRelStyle(token_module, plt_kernel, $offsetY="-30", $offsetX="0") +UpdateRelStyle(token_module_queries, plt_kernel, $offsetY="-30", $offsetX="-34") + +UpdateLayoutConfig($c4ShapeInRow="4", $c4BoundaryInRow="2") +``` \ No newline at end of file diff --git a/plt/docs/migrate_p11.md b/plt/docs/migrate_p11.md new file mode 100644 index 0000000000..4af29c2c54 --- /dev/null +++ b/plt/docs/migrate_p11.md @@ -0,0 +1,45 @@ +# Rust PLT Scheduler P11 Migration + +The switch to use the Rust PLT Scheduler will happen at the protocol upgrade to P11. And not +at the node software update. This is to reduce risk of differences between the Rust and Haskell implementations. +The P9/10 and P11 block states will be compatible though. Long term (after the P11 migration on mainnet), +we way want to change to dispatch to the Rust PLT Scheduler also for P9 and P10 in order to clean +up the code. We can do a catchup on mainnet using the Rust PLT scheduler, to ensure that it is compatible +with the Haskell implementation. + +```mermaid +C4Container + title PLT logic per protocol version + Container_Boundary(node, "Node (Haskell)") { + Component(scheduler, "Scheduler and Queries") + + Component(plt_scheduler_haskell, "PLT Scheduler Logic and Queries (Haskell)") + + Boundary(plt_scheduler_boundary_rust, "PLT Scheduler (Rust) (FFI)", "boundary") { + Component(plt_scheduler, "PLT Scheduler and Queries (Rust)") + } + + Boundary(block_state_boundary, "Block state", "boundary") { + ComponentDb(block_state_9, "Block state P9") + ComponentDb(block_state_10, "Block state P10") + ComponentDb(block_state_11, "Block state P11") + } + } + +Rel(scheduler, plt_scheduler_haskell, "Dispatch on P9 and P10") +Rel(scheduler, plt_scheduler, "Dispatch on >=P11") + +Rel(plt_scheduler, block_state_11, "Updates and queries block state") +Rel(plt_scheduler_haskell, block_state_10, "Updates and queries block state") +Rel(plt_scheduler_haskell, block_state_9, "Updates and queries block state") + +UpdateElementStyle(scheduler, $borderColor="black") +UpdateElementStyle(plt_scheduler, $borderColor="black") +UpdateElementStyle(plt_scheduler_haskell, $borderColor="black") + +UpdateElementStyle(block_state_9, $borderColor="black") +UpdateElementStyle(block_state_10, $borderColor="black") +UpdateElementStyle(block_state_11, $borderColor="black") + +UpdateLayoutConfig($c4ShapeInRow="5", $c4BoundaryInRow="3") +``` \ No newline at end of file diff --git a/plt/node-rust-library/Cargo.toml b/plt/node-rust-library/Cargo.toml new file mode 100644 index 0000000000..8bfb91d2bd --- /dev/null +++ b/plt/node-rust-library/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "node-rust-library" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +[dependencies] +# Include the Rust scheduler and the smart contract engine/wasm chain integration in the library +plt-scheduler.workspace = true +concordium-smart-contract-engine = { features = ["enable-ffi"], workspace = true } + + diff --git a/plt/node-rust-library/src/lib.rs b/plt/node-rust-library/src/lib.rs new file mode 100644 index 0000000000..dbb4243568 --- /dev/null +++ b/plt/node-rust-library/src/lib.rs @@ -0,0 +1,4 @@ +// Include with extern crate to build in the functions exported with extern and +// whatever code is reachable from them. +extern crate concordium_smart_contract_engine; +extern crate plt_scheduler; diff --git a/plt/plt-block-state/Cargo.toml b/plt/plt-block-state/Cargo.toml new file mode 100644 index 0000000000..40e9778f52 --- /dev/null +++ b/plt/plt-block-state/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "plt-block-state" +version = "0.1.0" +edition = "2024" + +[features] +default = ["ffi"] +ffi = ["dep:libc"] + +[dependencies] +# Workspace dependencies +plt-scheduler-types.workspace = true + +# Concordium dependencies +concordium_base.workspace = true +concordium-smart-contract-engine.workspace = true + +# Third party dependencies +assert_matches.workspace = true +either.workspace = true +hex.workspace = true +im.workspace = true +libc = { workspace = true, optional = true } +sha2.workspace = true +thiserror.workspace = true + +[dev-dependencies] +assert_matches.workspace = true +divan.workspace = true +hex.workspace = true +rand.workspace = true + +[[bench]] +name = "lfmb_tree" +harness = false + +[[bench]] +name = "hashed_cacheable_reference" +harness = false diff --git a/plt/plt-block-state/benches/hashed_cacheable_reference.rs b/plt/plt-block-state/benches/hashed_cacheable_reference.rs new file mode 100644 index 0000000000..1f4f4174ee --- /dev/null +++ b/plt/plt-block-state/benches/hashed_cacheable_reference.rs @@ -0,0 +1,29 @@ +//! Benchmarks for the [`HashedCacheableRef`] covering congestion by concurrency. + +use divan::Bencher; +use plt_block_state::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use plt_block_state::persistent::blob_store::StoreSerialized; +use plt_block_state::persistent::blob_store::test_stub::UnreachableBlobStore; + +fn main() { + divan::main(); +} + +const THREADS: &[usize] = &[1, 2, 4, 8, 16]; + +/// Warmup CPU. Divan does not seem to be good at doing this automatically. +#[divan::bench] +fn a_warmup() { + let mut x: u64 = 0; + for i in divan::black_box(0..10_000_000) { + x = x.wrapping_add(i); + } + divan::black_box(x); +} + +/// Benchmark [`HashedCacheableRef::with_value`] for using different number of concurrent threads. +#[divan::bench(threads = THREADS)] +fn bench_with_value(bencher: Bencher) { + let hcr = divan::black_box(HashedCacheableRef::new(StoreSerialized(0))); + bencher.bench(|| hcr.value(&UnreachableBlobStore).unwrap()); +} diff --git a/plt/plt-block-state/benches/lfmb_tree.rs b/plt/plt-block-state/benches/lfmb_tree.rs new file mode 100644 index 0000000000..d0c2296034 --- /dev/null +++ b/plt/plt-block-state/benches/lfmb_tree.rs @@ -0,0 +1,129 @@ +//! Benchmarks for the [`LfmbTree`] covering asymptotic behavior with varying tree size. + +use divan::Bencher; +use plt_block_state::persistent::blob_store; +use plt_block_state::persistent::blob_store::StoreSerialized; +use plt_block_state::persistent::blob_store::test_stub::{BlobStoreStub, UnreachableBlobStore}; +use plt_block_state::persistent::cacheable::Cacheable; +use plt_block_state::persistent::hash::Hashable; +use plt_block_state::persistent::lfmb_tree::{LfmbTree, LfmbTreeKey}; + +fn main() { + divan::main(); +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct BenchKey(u64); + +impl LfmbTreeKey for BenchKey { + fn to_u64(self) -> u64 { + self.0 + } + + fn from_u64(key: u64) -> Self { + Self(key) + } +} + +type BenchTree = LfmbTree>; + +/// Sizes of trees to benchmark operations on. +const SIZES: &[u64] = &[(1 << 10) - 1, (1 << 15) - 1, (1 << 20) - 1]; + +/// Small sizes of trees to benchmark operations on. Used for benchmarks that needs +/// to create the full tree for each iteration. +const SMALL_SIZES: &[u64] = &[1 << 5, 1 << 10, 1 << 15]; + +fn build_tree(size: u64) -> BenchTree { + let mut tree = BenchTree::empty(); + for i in 0..size { + (_, tree) = tree + .insert_value(&UnreachableBlobStore, StoreSerialized(i)) + .unwrap(); + } + tree +} + +/// Warmup CPU. Divan does not seem to be good at doing this automatically. +#[divan::bench] +fn a_warmup() { + let mut x: u64 = 0; + for i in divan::black_box(0..10_000_000) { + x = x.wrapping_add(i); + } + divan::black_box(x); +} + +/// Benchmark [`LfmbTree::lookup_value`] for different tree sizes. +#[divan::bench(args = SIZES)] +fn bench_lookup_value(bencher: Bencher, size: u64) { + let tree = divan::black_box(build_tree(size)); + bencher + .with_inputs(|| BenchKey(rand::random_range(0..size))) + .bench_local_values(|key| { + tree.lookup_value(&UnreachableBlobStore, key) + .unwrap() + .unwrap() + }); +} + +/// Benchmark [`LfmbTree::insert_value`] by creating trees of different sizes. +#[divan::bench(args = SMALL_SIZES)] +fn bench_insert_values(bencher: Bencher, size: u64) { + bencher.bench_local(|| build_tree(size)); +} + +/// Benchmark [`LfmbTree::update_value`] for different tree sizes. +#[divan::bench(args = SIZES)] +fn bench_update_value(bencher: Bencher, size: u64) { + let tree = divan::black_box(build_tree(size)); + bencher + .with_inputs(|| BenchKey(rand::random_range(0..size))) + .bench_local_values(|key| { + tree.update_value(&UnreachableBlobStore, key, |v| Ok(StoreSerialized(v.0 + 1))) + .unwrap() + .unwrap() + }); +} + +/// Benchmark [`LfmbTree::values`] iterator for different tree sizes. +#[divan::bench(args = SIZES)] +fn bench_values(bencher: Bencher, size: u64) { + let tree = divan::black_box(build_tree(size)); + bencher.bench_local(|| { + tree.values(&UnreachableBlobStore) + .map(|r| r.unwrap().1.0) + .sum::() + }); +} + +/// Benchmark [`LfmbTree::hash`] iterator for different tree sizes. +#[divan::bench(args = SMALL_SIZES)] +fn bench_hash(bencher: Bencher, size: u64) { + bencher + .with_inputs(|| build_tree(size)) + .bench_local_values(|tree| tree.hash(&UnreachableBlobStore).unwrap()); +} + +/// Benchmark [`LfmbTree::hash`] iterator for different tree sizes. +#[divan::bench(args = SMALL_SIZES)] +fn bench_store(bencher: Bencher, size: u64) { + bencher + .with_inputs(|| build_tree(size)) + .bench_local_values(|tree| blob_store::store_to_store(&mut BlobStoreStub::default(), tree)); +} + +/// Benchmark [`LfmbTree::hash`] iterator for different tree sizes. +#[divan::bench(args = SMALL_SIZES)] +fn bench_cache(bencher: Bencher, size: u64) { + let mut store = BlobStoreStub::default(); + let tree = build_tree(size); + let blob_ref = divan::black_box(blob_store::store_to_store(&mut store, tree)); + + bencher + .with_inputs(|| { + let tree: BenchTree = blob_store::load_from_store(&store, blob_ref).unwrap(); + tree + }) + .bench_local_values(|tree| tree.cache_reference_values(&store)); +} diff --git a/plt/plt-block-state/src/entity.rs b/plt/plt-block-state/src/entity.rs new file mode 100644 index 0000000000..c74486bf77 --- /dev/null +++ b/plt/plt-block-state/src/entity.rs @@ -0,0 +1,107 @@ +//! Entity model for block state. This defines the block state interface to +//! the scheduler and generally exposes a statically types model. + +use crate::external::ExternalBlockStateOperations; +use crate::persistent::blob_store::BlobStoreLoad; +use std::fmt::Debug; +use std::marker::PhantomData; + +pub mod accounts; +pub mod block_state; +pub mod protocol_level_locks; +pub mod protocol_level_tokens; + +/// Types needed to define the [`EntityContext`] +pub trait EntityContextTypes { + /// Type for externally managed block state interactions. + type ExternalBlockState: ExternalBlockStateOperations; + /// Type for blob store. + type Store: BlobStoreLoad; +} + +/// Concrete types for [`EntityContextTypes`] +#[derive(Debug, Default, Clone)] +pub struct EntityContextTypesWitness( + PhantomData<(ExternalBlockState, Store)>, +); + +impl EntityContextTypes + for EntityContextTypesWitness +{ + type ExternalBlockState = ExternalBlockState; + type Store = Store; +} + +/// Context needed to call functions on the block state and entities +/// in the block state. +#[derive(Debug, Default, Clone)] +pub struct EntityContext { + /// Externally managed block state + pub external: C::ExternalBlockState, + /// Blob store loader. + pub store: C::Store, +} + +pub mod entity_test_stub { + use crate::entity::block_state::p9::BlockStateP9; + use crate::entity::block_state::p11::BlockStateP11; + use crate::entity::{EntityContext, EntityContextTypes, EntityContextTypesWitness}; + use crate::external::test_stub::{ExternalBlockStateStub, UnreachableExternalBlockState}; + use crate::persistent::blob_store; + use crate::persistent::blob_store::BlobStoreLocation; + use crate::persistent::blob_store::test_stub::BlobStoreStub; + use crate::persistent::block_state::p9::PersistentBlockStateP9; + use crate::persistent::block_state::p11::PersistentBlockStateP11; + + type NoExternalBlockStateTypes = + EntityContextTypesWitness; + + /// Stubbed context with no external block state (will panic if accessed). + pub type StubbedNoExternalEntityContext = EntityContext; + + /// Create stubbed context with no external block state (will panic if accessed). + pub fn new_no_external_context() -> StubbedNoExternalEntityContext { + let blob_store = BlobStoreStub::default(); + EntityContext { + external: UnreachableExternalBlockState, + store: blob_store, + } + } + + type StubbedExternalBlockStateTypes = + EntityContextTypesWitness; + + /// Stubbed context with stubbed external block state. + pub type StubbedEntityContext = EntityContext; + + /// Create stubbed context with stubbed external block state. + pub fn new_stubbed_context() -> StubbedEntityContext { + let blob_store = BlobStoreStub::default(); + EntityContext { + external: ExternalBlockStateStub::default(), + store: blob_store, + } + } + + pub fn load_block_state_p9( + context: &EntityContext, + blob_ref: BlobStoreLocation, + ) -> BlockStateP9 { + let persistent_block_state: PersistentBlockStateP9 = + blob_store::load_from_store(&context.store, blob_ref).expect("load block state"); + BlockStateP9 { + persistent: persistent_block_state, + } + } + + pub fn load_block_state_p11( + context: &EntityContext, + blob_ref: BlobStoreLocation, + ) -> BlockStateP11 { + let persistent_block_state: PersistentBlockStateP11 = + blob_store::load_from_store(&context.store, blob_ref).expect("load block state"); + BlockStateP11 { + persistent: persistent_block_state, + } + } +} diff --git a/plt/plt-block-state/src/entity/accounts.rs b/plt/plt-block-state/src/entity/accounts.rs new file mode 100644 index 0000000000..37f9bfdfdf --- /dev/null +++ b/plt/plt-block-state/src/entity/accounts.rs @@ -0,0 +1,151 @@ +use crate::entity::{EntityContext, EntityContextTypes}; +use crate::external::{ + AccountNotFoundByAddressError, AccountNotFoundByIndexError, ExternalBlockStateOperations, + ExternalBlockStateQuery, OverflowError, RawTokenAmountDelta, TokenAccountState, +}; +use crate::persistent::protocol_level_tokens::p9::TokenIndex; +use concordium_base::base::AccountIndex; +use concordium_base::contracts_common::AccountAddress; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Account with its canonical address. +/// +/// The account is guaranteed to exist on chain, when holding an instance of this type. +#[derive(Debug)] +pub struct AccountWithCanonicalAddress { + /// Account on chain. + pub account: Account, + /// The canonical account address of the account, i.e. the address used as part of the + /// credential deployment and not an alias. + pub canonical_account_address: AccountAddress, +} + +/// Representation of block state account. +/// +/// The account is guaranteed to exist on chain, when holding an instance of this type. +#[derive(Debug, Clone)] +pub struct Account { + /// Account index for and account that we know exists in the block state. + pub(crate) account_index: AccountIndex, +} + +impl Account { + /// Create account from an account index for an account that must exist. + pub fn from_existing_account(account_index: AccountIndex) -> Self { + Self { account_index } + } + + /// Get the account index for the account. + pub fn account_index(&self) -> AccountIndex { + self.account_index + } + + /// Get the token balance of the account. + pub fn account_token_balance( + &self, + context: &EntityContext, + token_index: TokenIndex, + ) -> RawTokenAmount { + context + .external + .read_token_account_balance(self.account_index, token_index) + } + + /// Get token account states. It returns states for all tokens + /// that the account holds. + pub fn token_account_states( + &self, + context: &EntityContext, + ) -> impl Iterator { + context + .external + .token_account_states(self.account_index) + .into_iter() + } + + /// Update the token balance of an account. + /// + /// # Arguments + /// + /// - `token` The token to update. + /// - `account` The account to update. + /// - `amount_delta` The token balance delta. + /// + /// # Errors + /// + /// - [`OverflowError`] The update would overflow or underflow (result in negative balance) + /// the token balance on the account. + pub fn update_token_account_balance( + &self, + context: &mut EntityContext, + token_index: TokenIndex, + amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError> { + context + .external + .update_token_account_balance(self.account_index, token_index, amount_delta) + } + + /// Initialize the balance of the given account to zero if it didn't have a balance before. + /// It has the observable effect that the token is then returned when querying the tokens + /// for an account. Should be called if the token module account state is set, + /// in order to make sure the token is returned when querying token account info. + /// + /// If the account already has a balance for the token in context, the operation has no effect + /// + /// # Arguments + /// + /// - `token` The token to touch state for in the account. + /// - `account` The account to touch token state for. + pub fn touch_token_account( + &self, + context: &mut EntityContext, + token_index: TokenIndex, + ) { + context + .external + .touch_token_account(self.account_index, token_index) + } +} + +/// Trait that defines block state operations related to accounts. +pub trait Accounts { + /// Lookup the account using an account address. + fn account_by_address( + &self, + address: &AccountAddress, + ) -> Result; + + /// Lookup the account using an account index. Returns both the opaque account + /// representation and the account canonical address. + fn account_by_index( + &self, + account_index: AccountIndex, + ) -> Result; +} + +impl Accounts for EntityContext { + fn account_by_address( + &self, + address: &AccountAddress, + ) -> Result { + let account_index = self.external.account_index_by_account_address(address)?; + Ok(Account::from_existing_account(account_index)) + } + + fn account_by_index( + &self, + account_index: AccountIndex, + ) -> Result { + let canonical_account_address = self + .external + .account_canonical_address_by_account_index(account_index)?; + + let account = Account::from_existing_account(account_index); + + Ok(AccountWithCanonicalAddress { + account, + canonical_account_address, + }) + } +} diff --git a/plt/plt-block-state/src/entity/block_state.rs b/plt/plt-block-state/src/entity/block_state.rs new file mode 100644 index 0000000000..3f2565c3d4 --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state.rs @@ -0,0 +1,16 @@ +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::TokenId; + +pub mod migration; +pub mod p10; +pub mod p11; +pub mod p9; + +/// Account with given id does not exist +#[derive(Debug, thiserror::Error)] +#[error("Token with id {0} does not exist")] +pub struct TokenNotFoundByIdError(pub TokenId); + +/// Lock with given id does not exist +#[derive(Debug)] +pub struct LockNotFoundByIdError(pub LockId); diff --git a/plt/plt-block-state/src/entity/block_state/migration.rs b/plt/plt-block-state/src/entity/block_state/migration.rs new file mode 100644 index 0000000000..3f277061ac --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/migration.rs @@ -0,0 +1,121 @@ +use crate::entity::block_state::p9::BlockStateP9; +use crate::entity::block_state::p10::BlockStateP10; +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreStore}; +use crate::persistent::block_state::PersistentBlockState; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; + +pub mod p10_to_p11; +pub mod p9_to_p10; + +/// Migrate the PLT block state from protocol version to another. The persistent block state +/// is first "lifted" into an entity block state and then migrated. +/// +/// # Arguments +/// +/// - `block_state` Block state to migrate. Must be of a protocol version one lower than +/// `to_protocol_version`. +/// - `from_store` Blob store loader for the blob store we are migrating from. +/// - `to_store` Blob store loader and storer for the blob store we are migrating to. +/// - `to_protocol_version` Protocol version for the block state to migrate to.om +pub fn migrate( + block_state: PersistentBlockState, + from_store: &impl BlobStoreLoad, + to_store: &mut (impl BlobStoreStore + BlobStoreLoad), + to_protocol_version: ProtocolVersion, +) -> BlockStateResult { + match block_state { + PersistentBlockState::P9(persistent_block_state) => { + let block_state = BlockStateP9 { + persistent: persistent_block_state, + }; + let new_block_state = + p9_to_p10::migrate_from_p9_to_p10(block_state, from_store, to_store)?; + assert_eq!(to_protocol_version, ProtocolVersion::P10); + Ok(PersistentBlockState::P10(new_block_state.persistent)) + } + PersistentBlockState::P10(persistent_block_state) => { + let block_state = BlockStateP10 { + persistent: persistent_block_state, + }; + let new_block_state = + p10_to_p11::migrate_from_p10_to_p11(block_state, from_store, to_store)?; + assert_eq!(to_protocol_version, ProtocolVersion::P11); + Ok(PersistentBlockState::P11(new_block_state.persistent)) + } + PersistentBlockState::P11(_) => Err(BlockStateFailure::Invariant( + "migration of P11 block state not implemented".to_string(), + )), + } +} + +pub mod test_utils { + use super::*; + use crate::entity::block_state::p9::BlockStateP9; + use crate::entity::block_state::p10::BlockStateP10; + use crate::entity::block_state::p11::BlockStateP11; + use crate::entity::entity_test_stub; + use crate::entity::entity_test_stub::StubbedEntityContext; + use crate::persistent::blob_store; + use crate::persistent::block_state::PersistentBlockState; + + /// Migrate a P9 block state store in the test stub to P10. + pub fn migrate_p9_to_p10( + context: &mut StubbedEntityContext, + block_state: BlockStateP9, + ) -> (StubbedEntityContext, BlockStateP10) { + // Flush the source block state so all referenced blobs are present in the source store. + blob_store::store_to_store(&mut context.store, &block_state.persistent); + + let mut migrated_context = entity_test_stub::new_stubbed_context(); + migrated_context.external = context.external.clone(); + + // Migrate the block state + let migrated_persistent = migrate( + PersistentBlockState::P9(block_state.persistent), + &context.store, + &mut migrated_context.store, + ProtocolVersion::P10, + ) + .expect("migrate P9 to P10"); + + // Store and load the migrated block state + let blob_ref = + blob_store::store_to_store(&mut migrated_context.store, &migrated_persistent); + let migrated_block_state = BlockStateP10 { + persistent: blob_store::load_from_store(&migrated_context.store, blob_ref).unwrap(), + }; + + (migrated_context, migrated_block_state) + } + + /// Migrate a P10 block state store in the test stub to P11. + pub fn migrate_p10_to_p11( + context: &mut StubbedEntityContext, + block_state: BlockStateP10, + ) -> (StubbedEntityContext, BlockStateP11) { + // Flush the source block state so all referenced blobs are present in the source store. + blob_store::store_to_store(&mut context.store, &block_state.persistent); + + let mut migrated_context = entity_test_stub::new_stubbed_context(); + migrated_context.external = context.external.clone(); + + // Migrate the block state + let migrated_persistent = migrate( + PersistentBlockState::P10(block_state.persistent), + &context.store, + &mut migrated_context.store, + ProtocolVersion::P11, + ) + .expect("migrate P9 to P10"); + + // Store and load the migrated block state + let blob_ref = + blob_store::store_to_store(&mut migrated_context.store, &migrated_persistent); + let migrated_block_state = BlockStateP11 { + persistent: blob_store::load_from_store(&migrated_context.store, blob_ref).unwrap(), + }; + + (migrated_context, migrated_block_state) + } +} diff --git a/plt/plt-block-state/src/entity/block_state/migration/p10_to_p11.rs b/plt/plt-block-state/src/entity/block_state/migration/p10_to_p11.rs new file mode 100644 index 0000000000..6e7987532f --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/migration/p10_to_p11.rs @@ -0,0 +1,195 @@ +use crate::entity::block_state::p10::BlockStateP10; +use crate::entity::block_state::p11::BlockStateP11; +use crate::entity::protocol_level_tokens; +use crate::failure::BlockStateResult; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreStore}; +use crate::persistent::block_state::p11::PersistentBlockStateP11; +use crate::persistent::protocol_level_locks::p11::PersistentLocksP11; + +/// Migrate the P10 block state to P11. +pub fn migrate_from_p10_to_p11( + block_state_p10: BlockStateP10, + from_store: &impl BlobStoreLoad, + to_store: &mut (impl BlobStoreStore + BlobStoreLoad), +) -> BlockStateResult { + let new_tokens = protocol_level_tokens::migration::p10_to_p11::migrate_from_p10_to_p11( + block_state_p10.persistent.tokens, + from_store, + to_store, + )?; + + let new_persistent = PersistentBlockStateP11 { + tokens: HashedCacheableRef::new(new_tokens), + locks: HashedCacheableRef::new(PersistentLocksP11::default()), + }; + + Ok(BlockStateP11 { + persistent: new_persistent, + }) +} + +#[cfg(test)] +mod test { + use crate::entity::block_state::migration; + use crate::entity::block_state::p10::BlockStateP10; + use crate::entity::entity_test_stub; + use crate::entity::protocol_level_tokens::p11::Roles; + use crate::persistent::protocol_level_tokens::p9::TokenConfiguration; + use concordium_base::protocol_level_tokens::{TokenAdminRole, TokenModuleRef}; + use plt_scheduler_types::types::tokens::RawTokenAmount; + + /// Migrate block state from P10 to P11. + #[test] + fn test_migrate_p10_to_p11() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + + let governance_account = context.external.create_account(); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index1 = block_state + .create_token(&context, configuration1.clone()) + .unwrap(); + let mut token1 = block_state.token_by_index(&context, token_index1).unwrap(); + token1 + .token_p9_base + .set_governance_account(&context, governance_account.account_index) + .unwrap(); + token1 + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(100)); + token1 + .token_p9_base + .set_deny_list_enabled(&context) + .unwrap(); + token1.token_p9_base.set_burnable_enabled(&context).unwrap(); + token1 + .token_p9_base + .set_token_name(&context, "token1name") + .unwrap(); + block_state.update_token(&context, token1).unwrap(); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + let token_index2 = block_state + .create_token(&context, configuration2.clone()) + .unwrap(); + let mut token2 = block_state.token_by_index(&context, token_index2).unwrap(); + token2 + .token_p9_base + .set_governance_account(&context, governance_account.account_index) + .unwrap(); + token2 + .token_p9_base + .set_allow_list_enabled(&context) + .unwrap(); + token2.token_p9_base.set_mintable_enabled(&context).unwrap(); + block_state.update_token(&context, token2).unwrap(); + + // Migrate the block state + let (migrated_context, migrated_block_state) = + migration::test_utils::migrate_p10_to_p11(&mut context, block_state); + + // Assert on migrated block state + assert_eq!( + migrated_block_state + .plt_list(&migrated_context) + .unwrap() + .len(), + 2 + ); + let token1 = migrated_block_state + .token_by_id(&migrated_context, &"token1".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token1 + .token_p9_base + .get_governance_account_index(&migrated_context) + .unwrap(), + governance_account.account_index + ); + assert_eq!( + token1.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(100) + ); + assert!(token1.token_p9_base.has_deny_list(&migrated_context)); + assert!(token1.token_p9_base.is_burnable(&migrated_context)); + assert_eq!( + token1 + .token_p9_base + .get_token_name(&migrated_context) + .unwrap(), + "token1name" + ); + assert_eq!( + token1 + .token_p9_base + .token_configuration(&migrated_context) + .unwrap(), + configuration1 + ); + let token2 = migrated_block_state + .token_by_id(&migrated_context, &"token2".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token2 + .token_p9_base + .get_governance_account_index(&migrated_context) + .unwrap(), + governance_account.account_index + ); + assert_eq!( + token2.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + assert!(token2.token_p9_base.has_allow_list(&migrated_context)); + assert!(token2.token_p9_base.is_mintable(&migrated_context)); + assert_eq!( + token2 + .token_p9_base + .token_configuration(&migrated_context) + .unwrap(), + configuration2 + ); + + // Assert that migrated tokens have had new roles added + let new_roles1 = token1 + .get_account_roles(&migrated_context, governance_account.account_index) + .unwrap(); + let mut expected_roles1 = Roles::none(); + expected_roles1.assign(TokenAdminRole::UpdateAdminRoles); + expected_roles1.assign(TokenAdminRole::UpdateMetadata); + expected_roles1.assign(TokenAdminRole::Pause); + expected_roles1.assign(TokenAdminRole::Burn); + expected_roles1.assign(TokenAdminRole::UpdateDenyList); + assert_eq!(new_roles1, expected_roles1); + let new_roles2 = token2 + .get_account_roles(&migrated_context, governance_account.account_index) + .unwrap(); + let mut expected_roles2 = Roles::none(); + expected_roles2.assign(TokenAdminRole::UpdateAdminRoles); + expected_roles2.assign(TokenAdminRole::UpdateMetadata); + expected_roles2.assign(TokenAdminRole::Pause); + expected_roles2.assign(TokenAdminRole::Mint); + expected_roles2.assign(TokenAdminRole::UpdateAllowList); + assert_eq!(new_roles2, expected_roles2); + + // Assert no locks + assert_eq!( + migrated_block_state + .lock_list(&migrated_context) + .unwrap_or_default(), + vec![] + ); + } +} diff --git a/plt/plt-block-state/src/entity/block_state/migration/p9_to_p10.rs b/plt/plt-block-state/src/entity/block_state/migration/p9_to_p10.rs new file mode 100644 index 0000000000..fea84b1adf --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/migration/p9_to_p10.rs @@ -0,0 +1,111 @@ +use crate::entity::block_state::p9::BlockStateP9; +use crate::entity::block_state::p10::BlockStateP10; +use crate::failure::BlockStateResult; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreMovable, BlobStoreStore}; + +/// Migrate the P9 block state to P10. +pub fn migrate_from_p9_to_p10( + block_state_p9: BlockStateP9, + from_store: &impl BlobStoreLoad, + to_store: &mut impl BlobStoreStore, +) -> BlockStateResult { + // There are no changes to data, so just move to new blob store. + let new_persistent = block_state_p9 + .persistent + .move_blob_store(from_store, to_store)?; + + Ok(BlockStateP10 { + persistent: new_persistent, + }) +} + +#[cfg(test)] +mod test { + use crate::entity::block_state::migration; + use crate::entity::block_state::p9::BlockStateP9; + use crate::entity::entity_test_stub; + use crate::persistent::protocol_level_tokens::p9::TokenConfiguration; + use concordium_base::protocol_level_tokens::TokenModuleRef; + use plt_scheduler_types::types::tokens::RawTokenAmount; + + /// Migrate block state from P9 to P10. + #[test] + fn test_migrate_p9_to_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP9::default(); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index1 = block_state + .create_token(&context, configuration1.clone()) + .unwrap(); + let mut token1 = block_state.token_by_index(&context, token_index1).unwrap(); + token1 + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(100)); + token1 + .token_p9_base + .set_deny_list_enabled(&context) + .unwrap(); + token1 + .token_p9_base + .set_token_name(&context, "token1name") + .unwrap(); + block_state.update_token(&context, token1).unwrap(); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + let _token_index2 = block_state.create_token(&context, configuration2.clone()); + + // Migrate the block state + let (migrated_context, migrated_block_state) = + migration::test_utils::migrate_p9_to_p10(&mut context, block_state); + + // Assert on migrated block state + assert_eq!(migrated_block_state.plt_list(&migrated_context).len(), 2); + let token1 = migrated_block_state + .token_by_id(&migrated_context, &"token1".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token1.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(100) + ); + assert_eq!( + token1 + .token_p9_base + .token_configuration(&migrated_context) + .unwrap(), + configuration1 + ); + assert!(token1.token_p9_base.has_deny_list(&migrated_context)); + assert_eq!( + token1 + .token_p9_base + .get_token_name(&migrated_context) + .unwrap(), + "token1name" + ); + let token2 = migrated_block_state + .token_by_id(&migrated_context, &"token2".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token2.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + assert_eq!( + token2 + .token_p9_base + .token_configuration(&migrated_context) + .unwrap(), + configuration2 + ); + } +} diff --git a/plt/plt-block-state/src/entity/block_state/p10.rs b/plt/plt-block-state/src/entity/block_state/p10.rs new file mode 100644 index 0000000000..18d066aeef --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/p10.rs @@ -0,0 +1,4 @@ +use crate::entity::block_state::p9::BlockStateP9; + +/// P10 block state. +pub type BlockStateP10 = BlockStateP9; diff --git a/plt/plt-block-state/src/entity/block_state/p11.rs b/plt/plt-block-state/src/entity/block_state/p11.rs new file mode 100644 index 0000000000..e1d55d8f53 --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/p11.rs @@ -0,0 +1,216 @@ +use crate::entity::block_state::{LockNotFoundByIdError, TokenNotFoundByIdError}; +use crate::entity::protocol_level_locks::p11::LockP11; +use crate::entity::protocol_level_tokens::p11::TokenP11; +use crate::entity::{ + EntityContext, EntityContextTypes, protocol_level_locks, protocol_level_tokens, +}; +use crate::external::ExternalBlockStateOperations; +use crate::failure::BlockStateResult; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::block_state::p11::PersistentBlockStateP11; +use crate::persistent::protocol_level_locks::p11::LockConfiguration; +use crate::persistent::protocol_level_tokens::p9::{TokenConfiguration, TokenIndex}; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::TokenId; + +/// P11 block state. +#[derive(Debug, Default, Clone)] +pub struct BlockStateP11 { + /// Persistent block state. + pub persistent: PersistentBlockStateP11, +} + +impl BlockStateP11 { + /// Get the [`TokenId`]s of all protocol-level tokens. + pub fn plt_list( + &self, + context: &EntityContext, + ) -> BlockStateResult> { + protocol_level_tokens::p9::plt_list( + context, + &*self.persistent.tokens.value(&context.store)?, + ) + .collect::>>() + } + + /// Get the token associated with a [`TokenId`] (if it exists). + /// The token ID is case-insensitive when looking up tokens by token ID. + /// + /// If the token is changed, it must be written back with [`Self::update_token`] + /// for applying the changes. + /// + /// # Arguments + /// + /// - `token_id` The token id to get the [`Self::Token`] of. + pub fn token_by_id( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> BlockStateResult> { + let token_index_option = protocol_level_tokens::p9::token_index_by_id( + &*self.persistent.tokens.value(&context.store)?, + token_id, + ); + + let Some(token_index) = token_index_option else { + return Ok(Err(TokenNotFoundByIdError(token_id.clone()))); + }; + + self.token_by_index(context, token_index).map(Ok) + } + + /// Create a new token with the given configuration. The initial state will be empty + /// and the initial supply will be 0. Returns representation of the created token. + /// + /// # Arguments + /// + /// - `configuration` The configuration for the token. + pub fn create_token( + &mut self, + context: &EntityContext, + configuration: TokenConfiguration, + ) -> BlockStateResult { + let mut new_tokens = self.persistent.tokens.value(&context.store)?.into_owned(); + let token_index = + protocol_level_tokens::p9::create_token(context, &mut new_tokens, configuration)?; + self.persistent.tokens = HashedCacheableRef::new(new_tokens); + + Ok(token_index) + } + + /// Get the token with the given [`TokenIndex`]. + /// Returns a [`BlockStateFailure`] if the token does not exist. + /// + /// If the token is changed, it must be written back with [`Self::update_token`] + /// for applying the changes. + /// + /// # Arguments + /// + /// - `token_index` The index of the token. + pub fn token_by_index( + &self, + context: &EntityContext, + token_index: TokenIndex, + ) -> BlockStateResult { + let token_base = protocol_level_tokens::p9::token_by_index( + context, + &*self.persistent.tokens.value(&context.store)?, + token_index, + )?; + + Ok(TokenP11 { + token_p9_base: token_base, + }) + } + + /// Update the token in the block state. Any modifications + /// to [`TokenP11`] are only applied when the token is updated. + pub fn update_token( + &mut self, + context: &EntityContext, + token: TokenP11, + ) -> BlockStateResult<()> { + let mut new_tokens = self.persistent.tokens.value(&context.store)?.into_owned(); + protocol_level_tokens::p9::update_token(context, &mut new_tokens, token.token_p9_base)?; + self.persistent.tokens = HashedCacheableRef::new(new_tokens); + + Ok(()) + } + + /// 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. + pub fn increment_plt_update_instruction_sequence_number( + &mut self, + context: &mut EntityContext, + ) { + context.external.increment_plt_update_sequence_number() + } + + /// Create a new PLT lock with the given configuration. The initial state will be empty. + /// Returns [`BlockStateFailure::Invariant`] if a lock with the given id already exists. + /// + /// # Arguments + /// + /// - `lock_id` The ID of the PLT lock. + /// - `configuration` The configuration for the PLT lock. + pub fn create_lock( + &mut self, + context: &EntityContext, + configuration: LockConfiguration, + ) -> BlockStateResult<()> { + let mut new_locks = self.persistent.locks.value(&context.store)?.into_owned(); + protocol_level_locks::p11::create_lock(context, &mut new_locks, configuration)?; + + self.persistent.locks = HashedCacheableRef::new(new_locks); + + Ok(()) + } + + /// Delete the lock with the given [`LockId`] if it exists. Returns `true` if it existed, or + /// `false` if it did not exist. + /// + /// # Arguments + /// - `lock_id` The ID of the PLT lock to delete. + pub fn delete_lock( + &mut self, + context: &EntityContext, + lock_id: &LockId, + ) -> BlockStateResult { + let mut new_locks = self.persistent.locks.value(&context.store)?.into_owned(); + let existing = protocol_level_locks::p11::delete_lock(context, &mut new_locks, lock_id)?; + if existing { + // We only need to update the locks if a lock was actually deleted, + // otherwise we would be unnecessarily updating the block state. + self.persistent.locks = HashedCacheableRef::new(new_locks); + } + Ok(existing) + } + + /// Get the [`LockId`]s of all protocol-level locks registered on the chain at the + /// end of the block. + pub fn lock_list( + &self, + context: &EntityContext, + ) -> BlockStateResult> { + Ok(protocol_level_locks::p11::lock_list( + context, + &*self.persistent.locks.value(&context.store)?, + ) + .cloned() + .collect()) + } + + /// Get the lock associated with a [`LockId`] (if it exists). + /// + /// # Arguments + /// + /// - `lock_id` The lock id to get the [`Self::Lock`] of. + pub fn lock_by_id( + &self, + context: &EntityContext, + lock_id: &LockId, + ) -> BlockStateResult> { + let lock_option = protocol_level_locks::p11::lock_by_id( + context, + &*self.persistent.locks.value(&context.store)?, + lock_id.clone(), + )?; + + Ok(lock_option.ok_or_else(|| LockNotFoundByIdError(lock_id.clone()))) + } + + /// Update the lock in the block state. Any modifications + /// to [`LockP11`] are only applied when the lock is updated. + pub fn update_lock( + &mut self, + context: &EntityContext, + lock: LockP11, + ) -> BlockStateResult<()> { + let mut new_locks = self.persistent.locks.value(&context.store)?.into_owned(); + protocol_level_locks::p11::update_lock(context, &mut new_locks, lock)?; + self.persistent.locks = HashedCacheableRef::new(new_locks); + + Ok(()) + } +} diff --git a/plt/plt-block-state/src/entity/block_state/p9.rs b/plt/plt-block-state/src/entity/block_state/p9.rs new file mode 100644 index 0000000000..fd78315d62 --- /dev/null +++ b/plt/plt-block-state/src/entity/block_state/p9.rs @@ -0,0 +1,112 @@ +use crate::entity::block_state::TokenNotFoundByIdError; +use crate::entity::protocol_level_tokens::p9::TokenP9; +use crate::entity::{EntityContext, EntityContextTypes, protocol_level_tokens}; +use crate::external::ExternalBlockStateOperations; +use crate::failure::BlockStateResult; +use crate::persistent::block_state::p9::PersistentBlockStateP9; +use crate::persistent::protocol_level_tokens::p9::{TokenConfiguration, TokenIndex}; +use concordium_base::protocol_level_tokens::TokenId; + +/// P9 block state. +#[derive(Debug, Default, Clone)] +pub struct BlockStateP9 { + /// Persistent block state. + pub persistent: PersistentBlockStateP9, +} + +impl BlockStateP9 { + /// Get the [`TokenId`]s of all protocol-level tokens. + pub fn plt_list( + &self, + context: &EntityContext, + ) -> impl ExactSizeIterator> { + protocol_level_tokens::p9::plt_list(context, &self.persistent.tokens) + } + + /// Get the token associated with a [`TokenId`] (if it exists). + /// The token ID is case-insensitive when looking up tokens by token ID. + /// + /// If the token is changed, it must be written back with [`Self::update_token`] + /// for applying the changes. + /// + /// # Arguments + /// + /// - `token_id` The token id to get the [`Self::Token`] of. + pub fn token_by_id( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> BlockStateResult> { + let token_index_option = + protocol_level_tokens::p9::token_index_by_id(&self.persistent.tokens, token_id); + + let Some(token_index) = token_index_option else { + return Ok(Err(TokenNotFoundByIdError(token_id.clone()))); + }; + + self.token_by_index(context, token_index).map(Ok) + } + + /// Create a new token with the given configuration. The initial state will be empty + /// and the initial supply will be 0. Returns index of the created token. + /// + /// # Arguments + /// + /// - `configuration` The configuration for the token. + pub fn create_token( + &mut self, + context: &EntityContext, + configuration: TokenConfiguration, + ) -> BlockStateResult { + protocol_level_tokens::p9::create_token(context, &mut self.persistent.tokens, configuration) + } + + /// Get the token with the given [`TokenIndex`]. + /// Returns a [`BlockStateFailure`] if the token does not exist. + /// + /// If the token is changed, it must be written back with [`Self::update_token`] + /// for applying the changes. + /// + /// # Arguments + /// + /// - `token_index` The index of the token. + pub fn token_by_index( + &self, + context: &EntityContext, + token_index: TokenIndex, + ) -> BlockStateResult { + let token_base = protocol_level_tokens::p9::token_by_index( + context, + &self.persistent.tokens, + token_index, + )?; + + Ok(TokenP9 { + token_p9_base: token_base, + }) + } + + /// Update the token in the block state. Any modifications + /// to [`TokenP9`] are not applied before the token is updated. + pub fn update_token( + &mut self, + context: &EntityContext, + token: TokenP9, + ) -> BlockStateResult<()> { + protocol_level_tokens::p9::update_token( + context, + &mut self.persistent.tokens, + token.token_p9_base, + ) + } + + /// 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. + pub fn increment_plt_update_instruction_sequence_number( + &mut self, + context: &mut EntityContext, + ) { + context.external.increment_plt_update_sequence_number() + } +} diff --git a/plt/plt-block-state/src/entity/protocol_level_locks.rs b/plt/plt-block-state/src/entity/protocol_level_locks.rs new file mode 100644 index 0000000000..d7faebf981 --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_locks.rs @@ -0,0 +1 @@ +pub mod p11; diff --git a/plt/plt-block-state/src/entity/protocol_level_locks/p11.rs b/plt/plt-block-state/src/entity/protocol_level_locks/p11.rs new file mode 100644 index 0000000000..5eb939e4ef --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_locks/p11.rs @@ -0,0 +1,180 @@ +use crate::entity::{EntityContext, EntityContextTypes}; +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::StoreSerialized; +use crate::persistent::protocol_level_locks::p11::{ + LockConfiguration, LockIndex, PersistentLockP11, PersistentLocksP11, +}; +use crate::persistent::protocol_level_tokens::p9::TokenIndex; +use crate::utils; +use concordium_base::base::AccountIndex; +use concordium_base::protocol_level_locks::LockId; + +/// List all non-deleted lock ids in *no particular order*. +pub(crate) fn lock_list<'a, C: EntityContextTypes>( + _context: &EntityContext, + persistent_locks: &'a PersistentLocksP11, +) -> impl Iterator { + persistent_locks.lock_id_map.keys() +} + +pub(crate) fn create_lock( + context: &EntityContext, + persistent_locks: &mut PersistentLocksP11, + configuration: LockConfiguration, +) -> BlockStateResult<()> { + let lock_id = configuration.lock_id.clone(); + let persistent = PersistentLockP11 { + locked_balances: Default::default(), + configuration: HashedCacheableRef::new(StoreSerialized(configuration)), + }; + let (lock_index, updated_locks) = persistent_locks + .locks + .insert_value(&context.store, Some(persistent))?; + persistent_locks.locks = updated_locks; + let existing = persistent_locks + .lock_id_map + .insert(lock_id.clone(), lock_index); + if existing.is_some() { + return Err(BlockStateFailure::Invariant(format!( + "lock with id {:?} already exists", + lock_id + ))); + } + + Ok(()) +} + +pub(crate) fn delete_lock( + context: &EntityContext, + persistent_locks: &mut PersistentLocksP11, + lock_id: &LockId, +) -> BlockStateResult { + let Some(lock_index) = persistent_locks.lock_id_map.remove(lock_id) else { + return Ok(false); + }; + persistent_locks.locks = persistent_locks + .locks + .update_value(&context.store, lock_index, |_| Ok(None))? + .ok_or_else(|| { + BlockStateFailure::Invariant(format!("Lock not found by index: {:?}", lock_id)) + })?; + Ok(true) +} + +pub(crate) fn update_lock( + context: &EntityContext, + persistent_locks: &mut PersistentLocksP11, + lock: LockP11, +) -> BlockStateResult<()> { + persistent_locks.locks = persistent_locks + .locks + .update_value(&context.store, lock.lock_index, |_| { + Ok(Some(lock.persistent)) + })? + .ok_or_else(|| { + BlockStateFailure::Invariant(format!("Lock not found by index: {:?}", lock.lock_index)) + })?; + Ok(()) +} + +pub(crate) fn lock_by_id( + context: &EntityContext, + persistent_locks: &PersistentLocksP11, + lock_id: LockId, +) -> BlockStateResult> { + let Some(&lock_index) = persistent_locks.lock_id_map.get(&lock_id) else { + return Ok(None); + }; + let Some(persistent) = persistent_locks + .locks + .lookup_value(&context.store, lock_index)? + else { + return Err(BlockStateFailure::Invariant(format!( + "No lock entry found for lock index {} ({lock_id})", + lock_index.0 + ))); + }; + let Some(persistent) = persistent.to_owned() else { + // Lock is deleted. + return Ok(None); + }; + Ok(Some(LockP11 { + lock_index, + persistent, + })) +} + +/// Representation of protocol-level lock on P11 and later protocols with compatible model. +#[derive(Debug)] +pub struct LockP11 { + pub(crate) lock_index: LockIndex, + /// Persistent model of the protocol-level lock. + pub(crate) persistent: PersistentLockP11, +} + +impl LockP11 { + /// Get the internal block state index of the lock. + pub fn lock_index(&self) -> LockIndex { + self.lock_index + } + + /// Get the configuration of the protocol-level lock. + pub fn lock_configuration( + &self, + context: &EntityContext, + ) -> BlockStateResult> { + self.persistent + .configuration + .value(&context.store) + .map(|cow| cow.cow_project()) + } + + /// Get the set of account/token balances currently tracked under the lock. + /// + /// Each returned pair identifies an account and token for which the lock may + /// hold a non-zero locked balance. The corresponding amount is tracked in the + /// token module state. + pub fn lock_balance_refs(&self) -> Vec<(AccountIndex, TokenIndex)> { + self.persistent.locked_balances.0.iter().cloned().collect() + } + + /// Track that the lock holds a balance for the given account and token. + /// + /// This records the account/token pair in the lock state so it can later be + /// queried through [`Self::lock_balance_refs`]. + /// + /// # Arguments + /// + /// - `account_index` The index of the account whose locked balance is tracked. + /// - `token_index` Index of the token whose locked balance is tracked. + pub fn add_lock_balance_ref(&mut self, account_index: AccountIndex, token_index: TokenIndex) { + self.persistent + .locked_balances + .0 + .insert((account_index, token_index)); + } + + /// Stop tracking that the lock holds a balance for the given account and token. + /// This removes the account/token pair from the lock state, so it will no longer be + /// returned by [`Self::lock_balance_refs`]. + /// + /// # Arguments + /// + /// - `account_index` The index of the account whose locked balance is no longer tracked. + /// - `token_index` Index of the token whose locked balance is no longer tracked. + /// + /// # Returns + /// `true` if the account/token pair was previously tracked and has been removed, + /// `false` if the account/token pair was not previously tracked. + pub fn remove_lock_balance_ref( + &mut self, + account_index: AccountIndex, + token_index: TokenIndex, + ) -> bool { + self.persistent + .locked_balances + .0 + .remove(&(account_index, token_index)) + } +} diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens.rs b/plt/plt-block-state/src/entity/protocol_level_tokens.rs new file mode 100644 index 0000000000..79fbd2b9b3 --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens.rs @@ -0,0 +1,4 @@ +pub mod migration; +pub mod p11; +pub mod p9; +mod state_keys; diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens/migration.rs b/plt/plt-block-state/src/entity/protocol_level_tokens/migration.rs new file mode 100644 index 0000000000..6173f32eb6 --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens/migration.rs @@ -0,0 +1 @@ +pub mod p10_to_p11; diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens/migration/p10_to_p11.rs b/plt/plt-block-state/src/entity/protocol_level_tokens/migration/p10_to_p11.rs new file mode 100644 index 0000000000..cad317e177 --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens/migration/p10_to_p11.rs @@ -0,0 +1,97 @@ +use crate::entity::protocol_level_tokens::p9::TokenP9Base; +use crate::entity::protocol_level_tokens::p11::TokenP11; +use crate::entity::{EntityContext, EntityContextTypesWitness}; +use crate::external::test_stub::UnreachableExternalBlockState; +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreMovable, BlobStoreStore}; +use crate::persistent::lfmb_tree::LfmbTree; +use crate::persistent::protocol_level_tokens::p9::PersistentTokensP9; +use concordium_base::protocol_level_tokens::TokenAdminRole; + +/// Context initialized during migration with no access to external block state (will panic if accessed). +type MigrationEntityContextNoExternal<'a, L> = + EntityContext>; + +/// Migrate the P10 block state to P11. +pub fn migrate_from_p10_to_p11( + mut persistent_tokens: PersistentTokensP9, + from_store: &impl BlobStoreLoad, + to_store: &mut (impl BlobStoreStore + BlobStoreLoad), +) -> BlockStateResult { + let to_context = MigrationEntityContextNoExternal { + external: UnreachableExternalBlockState, + store: to_store, + }; + + let mut new_tokens = LfmbTree::empty(); + for item in persistent_tokens.tokens.values(from_store) { + let (token_index, persistent_token) = item?; + + let moved_persistent_token = + persistent_token.move_blob_store(from_store, to_context.store)?; + + let mut new_token = TokenP11 { + token_p9_base: TokenP9Base { + token_index, + mutable_key_value_state: moved_persistent_token + .key_value_state + .value(to_context.store)? + .thaw(), + persistent: moved_persistent_token, + }, + }; + + /// List new roles on P11 which are unaffected by which features are enabled. + const NEW_UNIVERSAL_ROLES_P11: &[TokenAdminRole] = &[ + TokenAdminRole::UpdateAdminRoles, + TokenAdminRole::Pause, + TokenAdminRole::UpdateMetadata, + ]; + + // The governance account should hold every role, except for disabled features, so we build a + // list of every enabled role and the mandatory roles. + let mut enabled_roles = Vec::from(NEW_UNIVERSAL_ROLES_P11); + + if new_token.token_p9_base.has_allow_list(&to_context) { + enabled_roles.push(TokenAdminRole::UpdateAllowList); + } + if new_token.token_p9_base.has_deny_list(&to_context) { + enabled_roles.push(TokenAdminRole::UpdateDenyList); + } + if new_token.token_p9_base.is_mintable(&to_context) { + enabled_roles.push(TokenAdminRole::Mint); + } + if new_token.token_p9_base.is_burnable(&to_context) { + enabled_roles.push(TokenAdminRole::Burn); + } + + let governance_account_index = new_token + .token_p9_base + .get_governance_account_index(&to_context)?; + new_token.assign_account_roles(&to_context, governance_account_index, &enabled_roles)?; + + if new_token.token_p9_base.mutable_key_value_state.is_dirty() { + new_token.token_p9_base.persistent.key_value_state = HashedCacheableRef::new( + new_token + .token_p9_base + .mutable_key_value_state + .freeze(to_context.store), + ); + } + + let new_token_index; + (new_token_index, new_tokens) = + new_tokens.insert_value(to_context.store, new_token.token_p9_base.persistent)?; + if new_token_index != token_index { + return Err(BlockStateFailure::Invariant(format!( + "token index changes from {:?} to {:?} during P10 to P11 migration", + token_index, new_token_index + ))); + } + } + + persistent_tokens.tokens = new_tokens; + + Ok(persistent_tokens) +} diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens/p11.rs b/plt/plt-block-state/src/entity/protocol_level_tokens/p11.rs new file mode 100644 index 0000000000..9e9ab82a37 --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens/p11.rs @@ -0,0 +1,349 @@ +use crate::entity::protocol_level_tokens::p9::TokenP9Base; +use crate::entity::protocol_level_tokens::state_keys; +use crate::entity::protocol_level_tokens::state_keys::ACCOUNT_ROLES_STATE_PREFIX; +use crate::entity::{EntityContext, EntityContextTypes}; +use crate::failure::{BlockStateFailure, BlockStateResult}; +use concordium_base::base::AccountIndex; +use concordium_base::common; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::TokenAdminRole; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Representation of protocol-level token on P11. +#[derive(Debug)] +pub struct TokenP11 { + /// Base P9 token representation + pub token_p9_base: TokenP9Base, +} + +impl TokenP11 { + /// Get the authorization roles for an account from state. + pub fn get_account_roles( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult { + Roles::try_from_state_value( + self.token_p9_base + .mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::account_roles_state_key(account), + ) + .as_deref(), + ) + .map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored account authorization roles cannot be decoded: {}", + err + )) + }) + } + + /// Update a value in the account section of the token state. + fn update_account_roles_state( + &mut self, + context: &EntityContext, + account: AccountIndex, + roles: Roles, + ) -> BlockStateResult<()> { + if let Some(value) = roles.into_state_value() { + self.token_p9_base.mutable_key_value_state.insert_value( + &context.store, + &state_keys::account_roles_state_key(account), + value, + ) + } else { + self.token_p9_base.mutable_key_value_state.delete_value( + &context.store, + &state_keys::account_roles_state_key(account), + ) + } + } + + /// Assign roles to an account in the state. + pub fn assign_account_roles( + &mut self, + context: &EntityContext, + account: AccountIndex, + roles_to_assign: &[TokenAdminRole], + ) -> BlockStateResult<()> { + let mut roles = self.get_account_roles(context, account)?; + for role in roles_to_assign { + roles.assign(*role) + } + self.update_account_roles_state(context, account, roles) + } + + /// Revoke roles of an account in the state. + pub fn revoke_account_roles( + &mut self, + context: &EntityContext, + account: AccountIndex, + roles_to_revoke: &[TokenAdminRole], + ) -> BlockStateResult<()> { + let mut roles = self.get_account_roles(context, account)?; + for role in roles_to_revoke { + roles.revoke(*role) + } + self.update_account_roles_state(context, account, roles) + } + + /// Get the locked balance for the given account and lock. + pub fn get_locked_balance_for_account( + &self, + context: &EntityContext, + account_index: AccountIndex, + lock_id: &LockId, + ) -> BlockStateResult { + let Some(value) = self.token_p9_base.mutable_key_value_state.lookup_value( + &context.store, + &state_keys::account_quanta_state_key(account_index, lock_id), + ) else { + return Ok(RawTokenAmount::from(0)); + }; + common::from_bytes_complete(value).map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Stored locked balance cannot be decoded: {}", + err + )) + }) + } + + /// Set the locked balance for the given account and lock. + pub fn set_locked_balance_for_account( + &mut self, + context: &EntityContext, + account_index: AccountIndex, + lock_id: &LockId, + amount: RawTokenAmount, + ) -> BlockStateResult<()> { + if amount == RawTokenAmount::from(0) { + self.token_p9_base.mutable_key_value_state.delete_value( + &context.store, + &state_keys::account_quanta_state_key(account_index, lock_id), + )?; + } else { + self.token_p9_base.mutable_key_value_state.insert_value( + &context.store, + &state_keys::account_quanta_state_key(account_index, lock_id), + common::to_bytes(&amount), + )?; + } + Ok(()) + } + + /// Get the locked balances recorded in token-module account state for the given + /// account. + pub fn get_locked_balances_for_account( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult> { + let prefix = state_keys::account_state_key(account, state_keys::ACCOUNT_STATE_KEY_QUANTA); + self.token_p9_base + .mutable_key_value_state + .iter_prefix(&context.store, &prefix)? + .map(move |(key, value)| { + let Some(lock_bytes) = key.strip_prefix(prefix.as_slice()) else { + return Err(BlockStateFailure::Invariant( + "Iterator over account quanta state produced invalid key".to_string(), + )); + }; + let lock = common::from_bytes_complete(lock_bytes).map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored lock id cannot be decoded: {}", + err + )) + })?; + let amount = common::from_bytes_complete(value).map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Stored locked balance cannot be decoded: {}", + err + )) + })?; + + Ok((lock, amount)) + }) + .collect() + } + + /// Iterate all authorization roles assigned for the token, together + /// with the account they are assigned to. + pub fn all_roles( + &self, + context: &EntityContext, + ) -> BlockStateResult> { + self.token_p9_base + .mutable_key_value_state + .iter_prefix(&context.store, &ACCOUNT_ROLES_STATE_PREFIX)? + .map(|(key, value)| { + let account_index_bytes = key + .strip_prefix(&ACCOUNT_ROLES_STATE_PREFIX) + .ok_or_else(|| { + BlockStateFailure::Invariant( + "Iterator over account roles state produced invalid key".to_string(), + ) + })?; + let account_index: AccountIndex = common::from_bytes_complete(account_index_bytes) + .map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored account index in authorizations cannot be decoded: {}", + err + )) + })?; + let roles = Roles::try_from_state_value(Some(&value)).map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored account authorization roles cannot be decoded: {}", + err + )) + })?; + + Ok((account_index, roles)) + }) + .collect() + } +} + +/// List all roles. +const ALL_ROLES: &[TokenAdminRole] = &[ + TokenAdminRole::UpdateAdminRoles, + TokenAdminRole::Mint, + TokenAdminRole::Burn, + TokenAdminRole::UpdateAllowList, + TokenAdminRole::UpdateDenyList, + TokenAdminRole::Pause, + TokenAdminRole::UpdateMetadata, +]; + +/// Convert a role into the bitmask with 1 in the position of the specific role and zero every else. +const fn role_bitmask(role: TokenAdminRole) -> u16 { + let bitshift = match role { + TokenAdminRole::UpdateAdminRoles => 0, + TokenAdminRole::Mint => 1, + TokenAdminRole::Burn => 2, + TokenAdminRole::UpdateAllowList => 3, + TokenAdminRole::UpdateDenyList => 4, + TokenAdminRole::Pause => 5, + TokenAdminRole::UpdateMetadata => 6, + }; + 1u16 << bitshift +} + +/// Collection of roles assigned to a single account. +#[derive(Debug, Eq, PartialEq, common::Serialize)] +pub struct Roles { + bitmap: u16, +} + +impl Roles { + /// Construct the collection with no roles assigned. + #[inline(always)] + pub const fn none() -> Self { + Self { bitmap: 0 } + } + + /// Test collection for no roles. + #[inline(always)] + fn has_none(&self) -> bool { + self.bitmap == 0 + } + + /// Test for a specific role being assigned. + #[inline(always)] + pub fn has(&self, role: TokenAdminRole) -> bool { + self.bitmap & role_bitmask(role) != 0 + } + + /// Set the specific role to be a assigned. + #[inline(always)] + pub const fn assign(&mut self, role: TokenAdminRole) { + self.bitmap |= role_bitmask(role); + } + + /// Unset the specific role. + #[inline(always)] + pub fn revoke(&mut self, role: TokenAdminRole) { + self.bitmap &= !role_bitmask(role); + } + + /// Convert into token state value representation. + /// + /// The empty set of roles results in `None`. + pub fn into_state_value(self) -> Option> { + // The state value is set to none for accounts without any roles. + if self.has_none() { + None + } else { + Some(common::to_bytes(&self)) + } + } + + /// Convert from token state value representation. + /// + /// State value of `None` results in the empty set of roles. + pub fn try_from_state_value(value: Option<&[u8]>) -> common::ParseResult { + let Some(value) = value else { + return Ok(Roles::none()); + }; + common::from_bytes_complete(value) + } + + /// Iterate the roles assigned. + pub fn iter_assigned(&self) -> impl Iterator { + ALL_ROLES.iter().filter(|&role| self.has(*role)).copied() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_revoke_roles() { + let mut roles = Roles::none(); + + roles.assign(TokenAdminRole::UpdateAdminRoles); + roles.assign(TokenAdminRole::Mint); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + assert!(roles.has(TokenAdminRole::Mint)); + + roles.revoke(TokenAdminRole::Mint); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + assert!(!roles.has(TokenAdminRole::Mint)); + assert!(!roles.has(TokenAdminRole::Burn)); + } + + #[test] + fn test_assign_roles() { + let mut roles = Roles::none(); + + assert!(!roles.has(TokenAdminRole::UpdateAdminRoles)); + roles.assign(TokenAdminRole::UpdateAdminRoles); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + + assert!(!roles.has(TokenAdminRole::Mint)); + roles.assign(TokenAdminRole::Mint); + assert!(roles.has(TokenAdminRole::Mint)); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + } + + #[test] + fn test_assign_roles_twice_is_nop() { + let mut roles = Roles::none(); + roles.assign(TokenAdminRole::UpdateAdminRoles); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + roles.assign(TokenAdminRole::UpdateAdminRoles); + assert!(roles.has(TokenAdminRole::UpdateAdminRoles)); + } + + #[test] + fn test_has_no_roles() { + let mut roles = Roles::none(); + assert!(roles.has_none()); + roles.assign(TokenAdminRole::UpdateAdminRoles); + assert!(!roles.has_none()); + roles.revoke(TokenAdminRole::UpdateAdminRoles); + assert!(roles.has_none()); + } +} diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens/p9.rs b/plt/plt-block-state/src/entity/protocol_level_tokens/p9.rs new file mode 100644 index 0000000000..945d6e445a --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens/p9.rs @@ -0,0 +1,465 @@ +use crate::entity::protocol_level_tokens::state_keys; +use crate::entity::protocol_level_tokens::state_keys::{ + STATE_KEY_ALLOW_LIST, STATE_KEY_BURNABLE, STATE_KEY_DENY_LIST, STATE_KEY_GOVERNANCE_ACCOUNT, + STATE_KEY_METADATA, STATE_KEY_MINTABLE, STATE_KEY_NAME, STATE_KEY_PAUSED, +}; +use crate::entity::{EntityContext, EntityContextTypes}; +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::StoreSerialized; +use crate::persistent::protocol_level_tokens::p9::{ + PersistentTokenP9, PersistentTokensP9, TokenConfiguration, TokenIndex, +}; +use crate::persistent::smart_contract_trie; +use crate::{persistent, utils}; +use concordium_base::base::AccountIndex; +use concordium_base::common; +use concordium_base::protocol_level_tokens::{MetadataUrl, TokenId}; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +pub(crate) fn plt_list( + context: &EntityContext, + persistent_tokens: &PersistentTokensP9, +) -> impl ExactSizeIterator> { + persistent_tokens.tokens.values(&context.store).map(|item| { + Ok(item? + .1 + .cow_project_configuration() + .value(&context.store)? + .into_owned() + .0 + .token_id) + }) +} + +pub(crate) fn create_token( + context: &EntityContext, + persistent_tokens: &mut PersistentTokensP9, + configuration: TokenConfiguration, +) -> BlockStateResult { + let normalized_token_id = + persistent::protocol_level_tokens::normalize_token_id(&configuration.token_id); + + let persistent_token = PersistentTokenP9 { + configuration: HashedCacheableRef::new(StoreSerialized(configuration)), + key_value_state: HashedCacheableRef::new(smart_contract_trie::PersistentState::empty()), + circulating_supply: StoreSerialized(RawTokenAmount::from(0)), + }; + + let token_index; + (token_index, persistent_tokens.tokens) = persistent_tokens + .tokens + .insert_value(&context.store, persistent_token)?; + persistent_tokens + .token_id_map + .insert(normalized_token_id, token_index); + + Ok(token_index) +} + +pub(crate) fn update_token( + context: &EntityContext, + persistent_tokens: &mut PersistentTokensP9, + mut token: TokenP9Base, +) -> BlockStateResult<()> { + if token.mutable_key_value_state.is_dirty() { + token.persistent.key_value_state = + HashedCacheableRef::new(token.mutable_key_value_state.freeze(&context.store)); + } + + persistent_tokens.tokens = persistent_tokens + .tokens + .update_value(&context.store, token.token_index, |_| Ok(token.persistent))? + .ok_or_else(|| { + BlockStateFailure::Invariant(format!( + "Token not found by index: {:?}", + token.token_index + )) + })?; + + Ok(()) +} + +pub(crate) fn token_by_index( + context: &EntityContext, + persistent_tokens: &PersistentTokensP9, + token_index: TokenIndex, +) -> BlockStateResult { + let persistent_token = persistent_tokens + .tokens + .lookup_value(&context.store, token_index)? + .ok_or_else(|| { + BlockStateFailure::Invariant(format!("Token not found by index: {:?}", token_index)) + })? + .into_owned(); + + let mutable_key_value_state = persistent_token + .key_value_state + .value(&context.store)? + .thaw(); + + Ok(TokenP9Base { + token_index, + persistent: persistent_token, + mutable_key_value_state, + }) +} + +pub(crate) fn token_index_by_id( + persistent_tokens: &PersistentTokensP9, + token_id: &TokenId, +) -> Option { + persistent_tokens + .token_id_map + .get(&persistent::protocol_level_tokens::normalize_token_id( + token_id, + )) + .copied() +} + +/// Representation of protocol-level token on P9 and P10. +#[derive(Debug)] +pub struct TokenP9 { + /// Base P9 token representation + pub token_p9_base: TokenP9Base, +} + +/// Base type for protocol-level token on P9 and later protocols with compatible model. +/// Protocol-specific token types (P9 and P11 currently) uses this type via composition +#[derive(Debug)] +pub struct TokenP9Base { + /// Token index + pub(crate) token_index: TokenIndex, + /// Persistent model of the protoco-level token. + pub(crate) persistent: PersistentTokenP9, + /// Token key-value state + pub(crate) mutable_key_value_state: smart_contract_trie::MutableState, +} + +impl TokenP9Base { + /// Get the index of the token. + pub fn token_index(&self) -> TokenIndex { + self.token_index + } + + /// Get the configuration of a protocol-level token. + pub fn token_configuration( + &self, + context: &EntityContext, + ) -> BlockStateResult { + Ok(self + .persistent + .configuration + .value(&context.store)? + .into_owned() + .0) + } + + /// Get the circulating supply of a protocol-level token. + pub fn token_circulating_supply(&self) -> RawTokenAmount { + self.persistent.circulating_supply.0 + } + + /// 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 + /// + /// - `circulation_supply` The new total circulating supply for the token. + pub fn set_token_circulating_supply(&mut self, circulation_supply: RawTokenAmount) { + self.persistent.circulating_supply.0 = circulation_supply; + } + + /// Get whether the balance-affecting operations on the token are currently + /// paused. + pub fn is_paused(&self, context: &EntityContext) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_PAUSED), + ) + .is_some() + } + + /// Sets the paused state of the token module. + pub fn set_paused( + &mut self, + context: &EntityContext, + value: bool, + ) -> BlockStateResult<()> { + if value { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_PAUSED), + vec![], + ) + } else { + self.mutable_key_value_state.delete_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_PAUSED), + ) + } + } + + /// Get whether the token has allow lists enabled. + pub fn has_allow_list(&self, context: &EntityContext) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_ALLOW_LIST), + ) + .is_some() + } + + /// Enabled 'allowList' feature for the token. + pub fn set_allow_list_enabled( + &mut self, + context: &EntityContext, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_ALLOW_LIST), + vec![], + ) + } + + /// Get whether the token has deny lists enabled. + pub fn has_deny_list(&self, context: &EntityContext) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_DENY_LIST), + ) + .is_some() + } + + /// Enabled 'DenyList' feature for the token. + pub fn set_deny_list_enabled( + &mut self, + context: &EntityContext, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_DENY_LIST), + vec![], + ) + } + + /// Get whether the token allows minting. + pub fn is_mintable(&self, context: &EntityContext) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_MINTABLE), + ) + .is_some() + } + + /// Enabled 'Mintable' feature for the token. + pub fn set_mintable_enabled( + &mut self, + context: &EntityContext, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_MINTABLE), + vec![], + ) + } + + /// Get whether the token allows burning. + pub fn is_burnable(&self, context: &EntityContext) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_BURNABLE), + ) + .is_some() + } + + /// Enabled 'Burnable' feature for the token. + pub fn set_burnable_enabled( + &mut self, + context: &EntityContext, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_BURNABLE), + vec![], + ) + } + + /// Get the name of the token. + pub fn get_token_name( + &self, + context: &EntityContext, + ) -> BlockStateResult { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_NAME), + ) + .ok_or_else(|| BlockStateFailure::Invariant("Name not present".to_string())) + .and_then(|value| { + String::from_utf8(value).map_err(|err| { + BlockStateFailure::Invariant(format!("Stored name is invalid UTF-8: {}", err)) + }) + }) + } + + /// Set the name of the token. + pub fn set_token_name( + &mut self, + context: &EntityContext, + name: &str, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_NAME), + name.as_bytes().to_vec(), + ) + } + + /// Get the account index of the governance account for the token. + pub fn get_governance_account_index( + &self, + context: &EntityContext, + ) -> BlockStateResult { + let governance_account_index = AccountIndex::from( + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_GOVERNANCE_ACCOUNT), + ) + .ok_or_else(|| { + BlockStateFailure::Invariant("Governance account not present".to_string()) + }) + .and_then(|value| { + common::from_bytes_complete::(value.as_slice()).map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored governance account index cannot be decoded: {}", + err + )) + }) + })?, + ); + Ok(governance_account_index) + } + + /// Set the token governance account in module state. + pub fn set_governance_account( + &mut self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult<()> { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_GOVERNANCE_ACCOUNT), + common::to_bytes(&account), + ) + } + + /// Get the URL metadata of the token. + pub fn get_metadata( + &self, + context: &EntityContext, + ) -> BlockStateResult { + let metadata_cbor = self + .mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_METADATA), + ) + .ok_or_else(|| BlockStateFailure::Invariant("Metadata not present".to_string()))?; + let metadata: MetadataUrl = utils::cbor_decode(metadata_cbor).map_err(|err| { + BlockStateFailure::Invariant(format!("Stored metadata CBOR not decodable: {}", err)) + })?; + Ok(metadata) + } + + /// Set the metadata URL. + pub fn set_metadata_url( + &mut self, + context: &EntityContext, + metadata: &MetadataUrl, + ) -> BlockStateResult<()> { + let encoded_metadata = common::cbor::cbor_encode(metadata); + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::module_state_key(STATE_KEY_METADATA), + encoded_metadata, + ) + } + + /// Get the allow-list state for the account at the given account. + pub fn get_allow_list_for( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_ALLOW_LIST), + ) + .is_some() + } + + /// Set the allow-list state for the account at the given account. + pub fn set_allow_list_for( + &mut self, + context: &EntityContext, + + account: AccountIndex, + value: bool, + ) -> BlockStateResult<()> { + if value { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_ALLOW_LIST), + vec![], + ) + } else { + self.mutable_key_value_state.delete_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_ALLOW_LIST), + ) + } + } + + /// Get the deny-list state for the account at the given account. + pub fn get_deny_list_for( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> bool { + self.mutable_key_value_state + .lookup_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_DENY_LIST), + ) + .is_some() + } + + /// Set the deny-list state for the account at the given account. + pub fn set_deny_list_for( + &mut self, + context: &EntityContext, + account: AccountIndex, + value: bool, + ) -> BlockStateResult<()> { + if value { + self.mutable_key_value_state.insert_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_DENY_LIST), + vec![], + ) + } else { + self.mutable_key_value_state.delete_value( + &context.store, + &state_keys::account_state_key(account, STATE_KEY_DENY_LIST), + ) + } + } +} diff --git a/plt/plt-block-state/src/entity/protocol_level_tokens/state_keys.rs b/plt/plt-block-state/src/entity/protocol_level_tokens/state_keys.rs new file mode 100644 index 0000000000..117aeae99a --- /dev/null +++ b/plt/plt-block-state/src/entity/protocol_level_tokens/state_keys.rs @@ -0,0 +1,107 @@ +//! Internal constants and utilities for token key-value state. + +use concordium_base::protocol_level_locks::LockId; +use concordium_base::{base::AccountIndex, common::Serial}; + +/// Little-endian prefix used to distinguish module state keys. +const MODULE_STATE_PREFIX: [u8; 2] = 0u16.to_le_bytes(); + +/// Little-endian prefix used to distinguish account state keys. +const ACCOUNT_STATE_PREFIX: [u8; 2] = 40307u16.to_le_bytes(); + +/// Little-endian prefix used to distinguish account role state keys. +/// +/// Note the roles are stored separately from the remaining account state, to allow for iterating +/// the prefix. +pub(crate) const ACCOUNT_ROLES_STATE_PREFIX: [u8; 2] = 40308u16.to_le_bytes(); + +pub(crate) const STATE_KEY_NAME: &[u8] = b"name"; +pub(crate) const STATE_KEY_METADATA: &[u8] = b"metadata"; +pub(crate) const STATE_KEY_ALLOW_LIST: &[u8] = b"allowList"; +pub(crate) const STATE_KEY_DENY_LIST: &[u8] = b"denyList"; +pub(crate) const STATE_KEY_MINTABLE: &[u8] = b"mintable"; +pub(crate) const STATE_KEY_BURNABLE: &[u8] = b"burnable"; +pub(crate) const STATE_KEY_PAUSED: &[u8] = b"paused"; +pub(crate) const STATE_KEY_GOVERNANCE_ACCOUNT: &[u8] = b"governanceAccount"; +pub(crate) const ACCOUNT_STATE_KEY_QUANTA: &[u8] = b"quanta"; + +/// Construct a [`TokenStateKey`] for a module key. This prefixes the key to +/// distinguish it from other keys. +pub(crate) fn module_state_key(key: &[u8]) -> Vec { + let mut module_key = Vec::with_capacity(MODULE_STATE_PREFIX.len() + key.len()); + module_key.extend_from_slice(&MODULE_STATE_PREFIX); + module_key.extend_from_slice(key); + module_key +} + +/// Construct a key for the account section of the token state. +pub(crate) fn account_state_key(account_index: AccountIndex, key: &[u8]) -> Vec { + let mut account_key = + Vec::with_capacity(ACCOUNT_STATE_PREFIX.len() + size_of::() + key.len()); + account_key.extend_from_slice(&ACCOUNT_STATE_PREFIX); + account_index.serial(&mut account_key); + account_key.extend_from_slice(key); + account_key +} + +/// Construct a key for the account roles section of the token state. +pub(crate) fn account_roles_state_key(account_index: AccountIndex) -> Vec { + let mut account_key = + Vec::with_capacity(ACCOUNT_ROLES_STATE_PREFIX.len() + size_of::()); + account_key.extend_from_slice(&ACCOUNT_ROLES_STATE_PREFIX); + account_index.serial(&mut account_key); + account_key +} + +/// Construct a key for the account quanta for the given lock +pub(crate) fn account_quanta_state_key(account_index: AccountIndex, lock_id: &LockId) -> Vec { + let mut locked_balance_key = + Vec::with_capacity(ACCOUNT_STATE_KEY_QUANTA.len() + size_of::()); + locked_balance_key.extend_from_slice(ACCOUNT_STATE_KEY_QUANTA); + lock_id.serial(&mut locked_balance_key); + account_state_key(account_index, &locked_balance_key) +} + +#[cfg(test)] +mod test { + use super::*; + + /// Test that the module state key is formed correctly + #[test] + fn test_module_state_key() { + let key = module_state_key(&[1, 2, 3]); + assert_eq!(key, vec![0, 0, 1, 2, 3]); + } + + /// Test that the account state key is formed correctly + #[test] + fn test_account_state_key() { + let key = account_state_key(AccountIndex::from(1u64), &[1, 2, 3]); + assert_eq!(key, vec![115, 157, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3]); + } + + /// Test that the account roles state key is formed correctly + #[test] + fn test_account_roles_state_key() { + let key = account_roles_state_key(AccountIndex::from(1u64)); + assert_eq!(key, vec![116, 157, 0, 0, 0, 0, 0, 0, 0, 1]); + } + + #[test] + fn test_account_quanta_state_key() { + let account = AccountIndex::from(1u64); + let lock_id = LockId { + account_index: 7, + sequence_number: 11, + creation_order: 3, + }; + + let mut expected = Vec::new(); + expected.extend_from_slice(&ACCOUNT_STATE_PREFIX); + account.serial(&mut expected); + expected.extend_from_slice(ACCOUNT_STATE_KEY_QUANTA); + lock_id.serial(&mut expected); + + assert_eq!(account_quanta_state_key(account, &lock_id), expected); + } +} diff --git a/plt/plt-block-state/src/external.rs b/plt/plt-block-state/src/external.rs new file mode 100644 index 0000000000..6cd150fcfc --- /dev/null +++ b/plt/plt-block-state/src/external.rs @@ -0,0 +1,332 @@ +//! Interactions with the part of the block state that is managed externally in Haskell. + +use crate::persistent::protocol_level_tokens::p9::TokenIndex; +use concordium_base::base::AccountIndex; +use concordium_base::common::Serialize; +use concordium_base::contracts_common::AccountAddress; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Change in [`RawTokenAmount`]. +/// +/// Represented as either add and subtract instead of a signed value, in order +/// to be able to represent the full range of possible deltas. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum RawTokenAmountDelta { + /// Add the token amount + Add(RawTokenAmount), + /// Subtract the token amount + Subtract(RawTokenAmount), +} + +/// Token account state at block state level. +/// +/// Corresponding Haskell type: `Concordium.GlobalState.Persistent.Account.ProtocolLevelTokens.TokenAccountState` +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +pub struct TokenAccountState { + /// Balance of the account + pub balance: RawTokenAmount, +} + +/// The computation resulted in overflow (negative or above maximum value). +#[derive(Debug, thiserror::Error)] +#[error("Token amount overflow")] +pub struct OverflowError; + +/// Account with given address does not exist +#[derive(Debug, thiserror::Error)] +#[error("Account with address {0} does not exist")] +pub struct AccountNotFoundByAddressError(pub AccountAddress); + +/// Account with given index does not exist +#[derive(Debug, thiserror::Error)] +#[error("Account with index {0} does not exist")] +pub struct AccountNotFoundByIndexError(pub AccountIndex); + +/// Type definition for queries to externally managed parts of the block state. +/// This state is managed in Haskell. +pub trait ExternalBlockStateQuery { + /// Read the account token balance from the block state. + /// + /// # Arguments + /// + /// - `account_index` The index of the account to update a token balance for. + /// Must be a valid account index of an existing account. + /// - `token_index` The index of the token. Must be a valid token index of an existing token. + fn read_token_account_balance( + &self, + account: AccountIndex, + token: TokenIndex, + ) -> RawTokenAmount; + + /// Get account canonical address by account index. Returns an error + /// if the account does not exist. + /// + /// # Arguments + /// + /// - `account_index` Index of the (possibly existing) account to get. + fn account_canonical_address_by_account_index( + &self, + account_index: AccountIndex, + ) -> Result; + + /// Get account index by account address (canonical address or alias address). + /// Returns an error if the account does not exist. + /// + /// # Arguments + /// + /// - `account_address` Address of the (possibly existing) account to get. + fn account_index_by_account_address( + &self, + account_address: &AccountAddress, + ) -> Result; + + /// Get token account states for an account. Returns pairs of the token index and the + /// token account state for the token. + /// + /// # Arguments + /// + /// - `account_index` The index of the account to get token account states for. Must be a valid account index of an existing account. + fn token_account_states( + &self, + account_index: AccountIndex, + ) -> Vec<(TokenIndex, TokenAccountState)>; +} + +/// Type definition for operations to externally managed parts of the block state. +/// This state is managed in Haskell. +pub trait ExternalBlockStateOperations: ExternalBlockStateQuery { + /// Update the account token balance in the block state. + /// Returns an error if the balance change would result in a negative balance + /// or a balance above the representable amount. + /// + /// # Arguments + /// + /// - `account_index` The index of the account to update a token balance for. Must be a valid account index of an existing account. + /// - `token_index` The index of the token. Must be a valid token index of an existing token. + /// - `amount_delta` The amount to add to or subtract from the balance. + fn update_token_account_balance( + &mut self, + account: AccountIndex, + token: TokenIndex, + amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError>; + + /// Initialize the balance of the given account to zero if it didn't have a balance before. + /// If the account already has a balance for the token in context, the operation has no effect + /// + /// # Arguments + /// + /// - `account_index` The index of the account to update a token balance for. Must be a valid account index of an existing account. + /// - `token_index` The index of the token. Must be a valid token index of an existing token. + fn touch_token_account(&mut self, account: AccountIndex, token: TokenIndex); + + /// Increment the PLT chain update sequence number. + fn increment_plt_update_sequence_number(&mut self); +} + +/// External block state stubs to be used in tests. +pub mod test_stub { + use super::*; + use crate::entity::accounts::Account; + use std::collections::BTreeMap; + + /// Non-accessible block state representing the Haskell maintained part of the block state. + #[derive(Debug, Default, Clone)] + pub struct UnreachableExternalBlockState; + + impl ExternalBlockStateQuery for UnreachableExternalBlockState { + fn read_token_account_balance( + &self, + _account: AccountIndex, + _token: TokenIndex, + ) -> RawTokenAmount { + unreachable!() + } + + fn account_canonical_address_by_account_index( + &self, + _account_index: AccountIndex, + ) -> Result { + unreachable!() + } + + fn account_index_by_account_address( + &self, + _account_address: &AccountAddress, + ) -> Result { + unreachable!() + } + + fn token_account_states( + &self, + _account_index: AccountIndex, + ) -> Vec<(TokenIndex, TokenAccountState)> { + unreachable!() + } + } + + impl ExternalBlockStateOperations for UnreachableExternalBlockState { + fn update_token_account_balance( + &mut self, + _account: AccountIndex, + _token: TokenIndex, + _amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError> { + unreachable!() + } + + fn touch_token_account(&mut self, _account: AccountIndex, _token: TokenIndex) { + unreachable!() + } + + fn increment_plt_update_sequence_number(&mut self) { + unreachable!() + } + } + + /// Stubbed block state representing the Haskell maintained part of the block state. + #[derive(Debug, Default, Clone)] + pub struct ExternalBlockStateStub { + /// List of accounts in the stub. + accounts: Vec, + /// PLT update instruction sequence number + plt_update_instruction_sequence_number: u64, + } + + impl ExternalBlockStateStub { + /// Create account in the stub and return stub representation of the account. + pub fn create_account(&mut self) -> Account { + let index = self.accounts.len(); + let mut address = AccountAddress([0u8; 32]); + address.0[..8].copy_from_slice(&index.to_be_bytes()); + let account = AccountStub { + address, + tokens: Default::default(), + }; + let stub_index = AccountIndex::from(index as u64); + self.accounts.push(account); + + Account::from_existing_account(stub_index) + } + + /// Get the canonical address of an account in the stub + pub fn account_canonical_address(&self, account: AccountIndex) -> AccountAddress { + self.accounts[account.index as usize].address + } + + /// Get next PLT update sequence number + pub fn plt_update_instruction_sequence_number(&self) -> u64 { + self.plt_update_instruction_sequence_number + } + } + + /// Internal representation of an account in [`BlockStateWithExternalStateStubbed`]. + #[derive(Debug, Clone)] + struct AccountStub { + /// The canonical account address of the account. + address: AccountAddress, + /// Tokens the account is holding + tokens: BTreeMap, + } + + /// Internal representation of a token in an account. + #[derive(Debug, Default, Clone)] + struct AccountTokenStub { + /// Account balance + balance: RawTokenAmount, + } + + impl ExternalBlockStateQuery for ExternalBlockStateStub { + fn read_token_account_balance( + &self, + account: AccountIndex, + token: TokenIndex, + ) -> RawTokenAmount { + self.accounts[account.index as usize] + .tokens + .get(&token) + .map(|token| token.balance) + .unwrap_or_default() + } + + fn account_canonical_address_by_account_index( + &self, + account_index: AccountIndex, + ) -> Result { + if let Some(account) = self.accounts.get(account_index.index as usize) { + Ok(account.address) + } else { + Err(AccountNotFoundByIndexError(account_index)) + } + } + + fn account_index_by_account_address( + &self, + account_address: &AccountAddress, + ) -> Result { + self.accounts + .iter() + .enumerate() + .find_map(|(i, account)| { + if account.address.is_alias(account_address) { + Some(AccountIndex::from(i as u64)) + } else { + None + } + }) + .ok_or(AccountNotFoundByAddressError(*account_address)) + } + + fn token_account_states( + &self, + account_index: AccountIndex, + ) -> Vec<(TokenIndex, TokenAccountState)> { + self.accounts[account_index.index as usize] + .tokens + .iter() + .map(|(token, state)| { + let token_account_state = TokenAccountState { + balance: state.balance, + }; + + (*token, token_account_state) + }) + .collect() + } + } + + impl ExternalBlockStateOperations for ExternalBlockStateStub { + fn update_token_account_balance( + &mut self, + account: AccountIndex, + token: TokenIndex, + amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError> { + let balance = &mut self.accounts[account.index as usize] + .tokens + .entry(token) + .or_default() + .balance; + match amount_delta { + RawTokenAmountDelta::Add(add) => { + *balance = balance.checked_add(add).ok_or(OverflowError)?; + } + RawTokenAmountDelta::Subtract(subtract) => { + *balance = balance.checked_sub(subtract).ok_or(OverflowError)?; + } + } + Ok(()) + } + + fn touch_token_account(&mut self, account: AccountIndex, token: TokenIndex) { + self.accounts[account.index as usize] + .tokens + .entry(token) + .or_default(); + } + + fn increment_plt_update_sequence_number(&mut self) { + self.plt_update_instruction_sequence_number += 1; + } + } +} diff --git a/plt/plt-block-state/src/failure.rs b/plt/plt-block-state/src/failure.rs new file mode 100644 index 0000000000..6017879be7 --- /dev/null +++ b/plt/plt-block-state/src/failure.rs @@ -0,0 +1,28 @@ +/// Unrecoverable failure accessing the block state. This is generally an error that +/// should never happen and is unrecoverable. +/// +/// If returned when **applying a block item to the block state**, +/// it may leave the block state in an indeterminate state. E.g. can parts of the effects +/// of processing the block item be applied, an others not. Hence, the resulting block +/// state should not be used. +/// +/// If returned when **querying the block state**, the query itself fails, +/// but the block state is still in a valid state. +#[derive(Debug, thiserror::Error)] +pub enum BlockStateFailure { + /// An error happened when decoding a block state value from the blob store. + #[error("Error decoding state from blob store: {0}")] + BlobStoreDecode(String), + /// An invariant that must be true is broken. The invariant can either be in the + /// stored block state or a runtime logical invariant related to the in-memory block state. + #[error("State invariant broken: {0}")] + Invariant(String), + /// When looking up a value with in an owned + /// [blob reference](super::block_state::blob_reference::hashed_cacheable_reference::HashedCacheableRef), + /// a borrowed value was returned. This should generally never happen in they way we maintain + /// blob references. + #[error("Borrowed value found inside of owned value: {0}")] + CowJoin(&'static str), +} + +pub type BlockStateResult = Result; diff --git a/plt/plt-block-state/src/ffi.rs b/plt/plt-block-state/src/ffi.rs new file mode 100644 index 0000000000..d67858e1c8 --- /dev/null +++ b/plt/plt-block-state/src/ffi.rs @@ -0,0 +1,10 @@ +//! This module provides a C ABI for the Rust PLT block state. +//! +//! It is only available if the `ffi` feature is enabled. + +pub mod blob_store_callbacks; +pub mod block_state; +pub mod block_state_callbacks; +pub mod external_chain_parameters; +pub mod memory; +pub mod status; diff --git a/plt/plt-block-state/src/ffi/blob_store_callbacks.rs b/plt/plt-block-state/src/ffi/blob_store_callbacks.rs new file mode 100644 index 0000000000..876605dddc --- /dev/null +++ b/plt/plt-block-state/src/ffi/blob_store_callbacks.rs @@ -0,0 +1,58 @@ +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreLocation, BlobStoreStore}; +use libc::size_t; + +/// A [loader](BlobStoreLoad) implemented by an external function. +/// This is the dual to [`StoreCallback`]. +/// +/// Returns pointer to a uniquely owned [`Vec`]. The `Vec` should be allocated +/// by the callee using `copy_to_vec_ffi` in `wasm-chain-integration`. +/// The returned `Vec` must be deallocated by the caller. +pub type LoadCallback = extern "C" fn(BlobStoreLocation) -> *mut Vec; + +/// A [storer](BlobStoreStore) implemented by an external function. +/// The function is passed a (shared) pointer to data to store, and the size of data. It +/// should return the location where the data can be loaded via a +/// [`LoadCallback`]. +pub type StoreCallback = extern "C" fn(data: *const u8, len: size_t) -> BlobStoreLocation; + +impl BlobStoreStore for StoreCallback { + fn store_raw(&mut self, data: impl AsRef<[u8]>) -> BlobStoreLocation { + let data_ref = data.as_ref(); + self(data_ref.as_ptr(), data_ref.len()) + } +} + +impl BlobStoreLoad for LoadCallback { + fn load_raw(&self, location: BlobStoreLocation) -> Vec { + *unsafe { Box::from_raw(self(location)) } + } +} + +/// Type representing blob store callbacks +pub struct BlobStoreCallbacks { + pub store_callback: StoreCallback, + pub load_callback: LoadCallback, +} + +impl BlobStoreStore for BlobStoreCallbacks { + fn store_raw(&mut self, data: impl AsRef<[u8]>) -> BlobStoreLocation { + self.store_callback.store_raw(data) + } +} + +impl BlobStoreLoad for BlobStoreCallbacks { + fn load_raw(&self, location: BlobStoreLocation) -> Vec { + self.load_callback.load_raw(location) + } +} + +pub mod tests_helpers { + use super::*; + + pub const UNIMPLEMENTED_LOAD_CALLBACK: LoadCallback = { + extern "C" fn fun(_: BlobStoreLocation) -> *mut Vec { + unimplemented!() + } + fun + }; +} diff --git a/plt/plt-block-state/src/ffi/block_state.rs b/plt/plt-block-state/src/ffi/block_state.rs new file mode 100644 index 0000000000..234da77964 --- /dev/null +++ b/plt/plt-block-state/src/ffi/block_state.rs @@ -0,0 +1,315 @@ +//! This module provides a C ABI for the Rust PLT block state. +//! +//! It is only available if the `ffi` feature is enabled. + +use super::status; +use crate::entity::block_state; +use crate::ffi::blob_store_callbacks::{BlobStoreCallbacks, LoadCallback, StoreCallback}; +use crate::persistent::blob_store; +use crate::persistent::blob_store::BlobStoreLocation; +use crate::persistent::block_state::PersistentBlockState; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; + +/// Allocate a new empty PLT block state. +/// +/// - [`status::FfiStatusCode::Success`]: Creating the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Creating the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `protocol_version` Protocol version for the block state to create. +/// - `block_state_out`: Location for writing the pointer of the new, empty block state. +/// The new block state is only written if return value is [`status::FfiStatusCode::Success`]. +/// The pointer written is to a uniquely owned instance. +/// The caller must free the written block state using `ffi_free_plt_block_state` when it is no longer used. +/// +/// # Safety +/// +/// - Argument `block_state_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_empty_plt_block_state( + protocol_version: u64, + block_state_out: *mut *mut PersistentBlockState, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + let protocol_version = + ProtocolVersion::try_from(protocol_version).expect("Unknown protocol version"); + let block_state = PersistentBlockState::empty(protocol_version); + unsafe { + *block_state_out = Box::into_raw(Box::new(block_state)); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Deallocate the PLT block state. +/// +/// This is called by the Haskell garbage collector, therefore we cannot handle +/// a status code unlike other FFI functions in this module. +/// +/// # Arguments +/// +/// - `block_state` Unique pointer to the PLT block state. +/// +/// # Safety +/// +/// - Argument `block_state` must be unique, non-null pointer to well-formed [`PersistentBlockState`]. +/// No other pointers to the block state must exist. +/// - Freeing is only ever done once. +#[unsafe(no_mangle)] +extern "C" fn ffi_free_plt_block_state(block_state: *mut PersistentBlockState) { + let panic_message = status::catch_unwind(move || { + assert!(!block_state.is_null(), "block_state is a null pointer."); + let state = unsafe { Box::from_raw(block_state) }; + drop(state); + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + } +} + +/// Compute the hash of the PLT block state. +/// +/// - [`status::FfiStatusCode::Success`]: Hashing the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Hashing the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `block_state` Shared pointer to the PLT block state to compute a hash for. +/// - `block_state_hash_out` Unique pointer with location to write the 32 bytes for the hash. +/// The hash is only written if return value is [`status::FfiStatusCode::Success`]. +/// +/// # Safety +/// +/// - Argument `load_callback` must be a valid function pointer to a function with a signature matching [`LoadCallback`]. +/// - Argument `block_state` must be a non-null pointer to well-formed [`PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `block_state_hash_out` must be non-null and valid for writes of 32 bytes. +#[unsafe(no_mangle)] +extern "C" fn ffi_hash_plt_block_state( + load_callback: LoadCallback, + block_state: *const PersistentBlockState, + block_state_hash_out: *mut u8, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !block_state_hash_out.is_null(), + "block_state_hash_out is a null pointer." + ); + let block_state = unsafe { &*block_state }; + let hash = block_state + .hash(&load_callback) + .expect("Failed hashing block state"); + unsafe { + std::ptr::copy_nonoverlapping(hash.as_ptr(), block_state_hash_out, hash.len()); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Load a PLT block state from the blob store and return it. +/// +/// - [`status::FfiStatusCode::Success`]: Loading the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Loading the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes from the blob store. +/// - `protocol_version` Protocol version for the block state to load. +/// - `blob_ref` Blob store reference to load the block state from. +/// - `block_state_out` Location for writing the pointer of the loaded block state. +/// The new block state is only written if return value is [`status::FfiStatusCode::Success`]. +/// The pointer written is to a uniquely owned instance. +/// The caller must free the written block state using `ffi_free_plt_block_state` when it is no longer used. +/// +/// # Safety +/// +/// - Argument `load_callback` must be a valid function pointer to a function with a signature matching [`LoadCallback`]. +/// - Argument `block_state_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_load_plt_block_state( + load_callback: LoadCallback, + blob_ref: BlobStoreLocation, + protocol_version: u64, + block_state_out: *mut *mut PersistentBlockState, +) -> status::FfiStatusCode { + assert!( + !block_state_out.is_null(), + "block_state_out is a null pointer." + ); + let panic_message = status::catch_unwind(move || { + let protocol_version = + ProtocolVersion::try_from(protocol_version).expect("Unknown protocol version"); + let block_state = + PersistentBlockState::load_from_store(&load_callback, blob_ref, protocol_version) + .expect("Failed loading the block state"); + unsafe { + *block_state_out = Box::into_raw(Box::new(block_state)); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Store a PLT block state in the blob store. +/// +/// - [`status::FfiStatusCode::Success`]: Storing the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Storing the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `store_callback` External function to call for storing bytes in the blob store returning a +/// reference. +/// - `blob_ref_out` Location for writing the blob store location the block state is stored to. +/// The block state location is only written if return value is [`status::FfiStatusCode::Success`]. +/// - `block_state` The block state to store in the blob store. +/// +/// # Safety +/// +/// - Argument `load_callback` must be a valid function pointer to a function with a signature matching [`LoadCallback`]. +/// - Argument `blob_ref_out` must be a non-null and valid pointer for writing +/// - Argument `block_state` must be a non-null pointer to well-formed [`PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +#[unsafe(no_mangle)] +extern "C" fn ffi_store_plt_block_state( + mut store_callback: StoreCallback, + blob_ref_out: *mut BlobStoreLocation, + block_state: *const PersistentBlockState, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!(!blob_ref_out.is_null(), "blob_ref_out is a null pointer."); + let block_state = unsafe { &*block_state }; + let reference = blob_store::store_to_store(&mut store_callback, block_state); + unsafe { + *blob_ref_out = reference; + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Migrate the PLT block state from one blob store to another. +/// +/// - [`status::FfiStatusCode::Success`]: Migrating the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Migrating the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `from_load_callback` External function to call for loading bytes a reference from +/// the blob store to migrate from. +/// - `to_store_callback` External function to call for storing bytes in the blob store +/// to migrate to. +/// - `to_load_callback` External function to call for loading bytes from the blob store +/// to migrate to. +/// - `to_protocol_version` Protocol version for the block state to migrate to. +/// - `new_block_state_out` Location for writing the pointer of the new, migrated block state. +/// The new block state is only written if return value is [`status::FfiStatusCode::Success`]. +/// The pointer written is to a uniquely owned instance. +/// The caller must free the written block state using `ffi_free_plt_block_state` when it is no longer used. +/// - `block_state` Shared pointer to a block state to migrate from. +/// +/// # Safety +/// +/// - Argument `load_callback` must be a valid function pointer to a function with a signature matching [`LoadCallback`]. +/// - Argument `store_callback` must be a valid function pointer to a function with a signature matching [`StoreCallback`]. +/// - Argument `new_block_state_out` must be a non-null and valid pointer for writing +/// - Argument `block_state` must be a non-null pointer to well-formed [`PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +#[unsafe(no_mangle)] +extern "C" fn ffi_migrate_plt_block_state( + from_load_callback: LoadCallback, + to_store_callback: StoreCallback, + to_load_callback: LoadCallback, + to_protocol_version: u64, + new_block_state_out: *mut *mut PersistentBlockState, + block_state: *const PersistentBlockState, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !new_block_state_out.is_null(), + "new_block_state_out is a null pointer." + ); + let from_block_state = unsafe { &*block_state }; + let to_protocol_version = + ProtocolVersion::try_from(to_protocol_version).expect("Unknown protocol version"); + let new_block_state = block_state::migration::migrate( + from_block_state.clone(), + &from_load_callback, + &mut BlobStoreCallbacks { + store_callback: to_store_callback, + load_callback: to_load_callback, + }, + to_protocol_version, + ) + .expect("Migrate block state"); + + unsafe { + *new_block_state_out = Box::into_raw(Box::new(new_block_state)); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Cache the PLT block state into memory. +/// +/// - [`status::FfiStatusCode::Success`]: Caching the block state was successful. +/// - [`status::FfiStatusCode::Panic`]: Caching the block state resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `block_state` The block state to store in the blob store. +/// +/// # Safety +/// +/// - Argument `load_callback` must be a valid function pointer to a function with a signature matching [`LoadCallback`]. +/// - Argument `block_state` must be a non-null pointer to well-formed [`PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +#[unsafe(no_mangle)] +extern "C" fn ffi_cache_plt_block_state( + load_callback: LoadCallback, + block_state: *const PersistentBlockState, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!block_state.is_null(), "block_state is a null pointer."); + let block_state = unsafe { &*block_state }; + block_state + .cache_reference_values(&load_callback) + .expect("Failed caching block state"); + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} diff --git a/plt/plt-block-state/src/ffi/block_state_callbacks.rs b/plt/plt-block-state/src/ffi/block_state_callbacks.rs new file mode 100644 index 0000000000..e5697107a0 --- /dev/null +++ b/plt/plt-block-state/src/ffi/block_state_callbacks.rs @@ -0,0 +1,331 @@ +use crate::external::{ + AccountNotFoundByAddressError, AccountNotFoundByIndexError, ExternalBlockStateOperations, + ExternalBlockStateQuery, OverflowError, RawTokenAmountDelta, TokenAccountState, +}; +use crate::persistent::protocol_level_tokens::p9::TokenIndex; +use concordium_base::base::AccountIndex; +use concordium_base::common; +use concordium_base::contracts_common::AccountAddress; +use plt_scheduler_types::types::tokens::RawTokenAmount; +use std::marker::PhantomData; + +/// Callbacks for block state queries. +#[derive(Debug, Clone)] +pub struct ExternalBlockStateQueryCallbacks { + /// External function for reading the token balance for an account. + pub read_token_account_balance_ptr: ReadTokenAccountBalanceCallback, + /// External function for fetching account address by index. + pub get_account_address_by_index_ptr: GetCanonicalAddressByAccountIndexCallback, + /// External function for fetching account index by address. + pub get_account_index_by_address_ptr: GetAccountIndexByAddressCallback, + /// External function for getting token account states. + pub get_token_account_states_ptr: GetTokenAccountStatesCallback, +} + +/// Callbacks for block state operations. +#[derive(Debug, Clone)] +pub struct ExternalBlockStateOperationCallbacks { + /// Callbacks for block state queries. + pub queries: ExternalBlockStateQueryCallbacks, + /// External function for updating the token balance for an account. + pub update_token_account_balance_ptr: UpdateTokenAccountBalanceCallback, + /// External function for touching the token state for an account. + pub touch_token_account_ptr: TouchTokenAccountCallback, + /// External function for incrementing the PLT update sequence number. + pub increment_plt_update_sequence_number_ptr: IncrementPltUpdateSequenceNumberCallback, + // The callbacks are not thread safe, hence we mark them as not Send nor Sync by using PhantomData<*const ()> + pub _not_send_sync: PhantomData<*const ()>, +} + +impl ExternalBlockStateQuery for ExternalBlockStateQueryCallbacks { + fn read_token_account_balance( + &self, + account: AccountIndex, + token: TokenIndex, + ) -> RawTokenAmount { + let value = (self.read_token_account_balance_ptr)(account.index, token.0); + + RawTokenAmount::from(value) + } + + fn account_canonical_address_by_account_index( + &self, + account_index: AccountIndex, + ) -> Result { + let mut account_address = AccountAddress([0; 32]); + + let result = (self.get_account_address_by_index_ptr)( + account_index.index, + account_address.0.as_mut_ptr(), + ); + + match result { + 0 => Ok(account_address), + 1 => Err(AccountNotFoundByIndexError(account_index)), + _ => panic!( + "Unrecognized result from GetCanonicalAddressByAccountIndexCallback: {}", + result + ), + } + } + + fn account_index_by_account_address( + &self, + account_address: &AccountAddress, + ) -> Result { + let mut account_index = AccountIndex { index: 0 }; + + let result = (self.get_account_index_by_address_ptr)( + account_address.0.as_ptr(), + &mut account_index.index, + ); + + match result { + 0 => Ok(account_index), + 1 => Err(AccountNotFoundByAddressError(*account_address)), + _ => panic!( + "Unrecognized result from GetAccountIndexByAddressCallback: {}", + result + ), + } + } + + fn token_account_states( + &self, + account_index: AccountIndex, + ) -> Vec<(TokenIndex, TokenAccountState)> { + let bytes = + unsafe { Box::from_raw((self.get_token_account_states_ptr)(account_index.index)) }; + common::from_bytes_complete(*bytes) + .expect("Invalid serialization of (TokenIndex, TokenAccountState) list") + } +} + +impl ExternalBlockStateOperations for ExternalBlockStateQueryCallbacks { + fn update_token_account_balance( + &mut self, + _account: AccountIndex, + _token: TokenIndex, + _amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError> { + panic!("operation callback called during query") + } + + fn touch_token_account(&mut self, _account: AccountIndex, _token: TokenIndex) { + panic!("operation callback called during query") + } + + fn increment_plt_update_sequence_number(&mut self) { + panic!("operation callback called during query") + } +} + +impl ExternalBlockStateOperations for ExternalBlockStateOperationCallbacks { + fn update_token_account_balance( + &mut self, + account: AccountIndex, + token: TokenIndex, + amount_delta: RawTokenAmountDelta, + ) -> Result<(), OverflowError> { + let result = (self.update_token_account_balance_ptr)( + account.index, + token.0, + match amount_delta { + RawTokenAmountDelta::Add(amount) => amount.value(), + RawTokenAmountDelta::Subtract(amount) => amount.value(), + }, + match amount_delta { + RawTokenAmountDelta::Add(_) => 1, + RawTokenAmountDelta::Subtract(_) => 0, + }, + ); + + match result { + 0 => Ok(()), + 1 => Err(OverflowError), + _ => panic!( + "Unrecognized result from UpdateTokenAccountBalanceCallback: {}", + result + ), + } + } + + fn touch_token_account(&mut self, account: AccountIndex, token: TokenIndex) { + (self.touch_token_account_ptr)(account.index, token.0); + } + + fn increment_plt_update_sequence_number(&mut self) { + (self.increment_plt_update_sequence_number_ptr)(); + } +} + +impl ExternalBlockStateQuery for ExternalBlockStateOperationCallbacks { + fn read_token_account_balance( + &self, + account: AccountIndex, + token: TokenIndex, + ) -> RawTokenAmount { + self.queries.read_token_account_balance(account, token) + } + + fn account_canonical_address_by_account_index( + &self, + account_index: AccountIndex, + ) -> Result { + self.queries + .account_canonical_address_by_account_index(account_index) + } + + fn account_index_by_account_address( + &self, + account_address: &AccountAddress, + ) -> Result { + self.queries + .account_index_by_account_address(account_address) + } + + fn token_account_states( + &self, + account_index: AccountIndex, + ) -> Vec<(TokenIndex, TokenAccountState)> { + self.queries.token_account_states(account_index) + } +} + +/// External function for updating the token balance for an account. +/// +/// Returns `0` if the balance change was applied, and `1` if the balance change would result in a +/// negative balance or a balance above the representable amount. +/// +/// # Arguments +/// +/// - `account_index` The index of the account to update a token balance for. Must be a valid +/// account index of an existing account. +/// - `token_index` The index of the token. Must be a valid token index of an existing token. +/// - `amount` The amount to add to or subtract from the balance. +/// - `add_amount` If `1`, the amount will be added to the balance. If `0`, it will be subtracted. +pub type UpdateTokenAccountBalanceCallback = + extern "C" fn(account_index: u64, token_index: u64, amount: u64, add_amount: u8) -> u8; + +/// External function for touching the token state for an account. +/// +/// +/// # Arguments +/// +/// - `account_index` The index of the account to update a token balance for. Must be a valid +/// account index of an existing account. +/// - `token_index` The index of the token. Must be a valid token index of an existing token. +pub type TouchTokenAccountCallback = extern "C" fn(account_index: u64, token_index: u64); + +/// External function for reading the token balance for an account. +/// +/// # Arguments +/// +/// - `account_index` The index of the account to update a token balance for. +/// Must be a valid account index of an existing account. +/// - `token_index` The index of the token. Must be a valid token index of an existing token. +pub type ReadTokenAccountBalanceCallback = + extern "C" fn(account_index: u64, token_index: u64) -> u64; + +/// External function for incrementing the PLT update instruction sequence number. +pub type IncrementPltUpdateSequenceNumberCallback = extern "C" fn(); + +/// External function for getting account canonical address by account index. +/// Returns `0` if the account exist, `1` if not. +/// If the account exists, its canonical address is written to `account_address_out`. +/// +/// # Arguments +/// +/// - `account_index` Index of the (possibly existing) account to get. +/// - `account_address_out` Pointer to where to write the canonical account address of 32 bytes. +/// The pointer is a unique pointer, but ownership transfers back to the caller when the function returns. +/// +/// # Safety +/// +/// - Argument `account_address_out` must be non-null and valid for writes of 32 bytes. +pub type GetCanonicalAddressByAccountIndexCallback = + extern "C" fn(account_index: u64, account_address_out: *mut u8) -> u8; + +/// External function for getting account index by account address (canonical address or alias address). +/// Returns `0` if the account exist, `1` if not. +/// If the account exists, its index is written to `account_index_out`. +/// +/// # Arguments +/// +/// - `account_address` Address 32 bytes of the (possibly existing) account to get. +/// - `account_index_out` Pointer to where to write account index. +/// +/// # Safety +/// +/// - Argument `account_address` must be non-null and valid for reads for 32 bytes. +/// - Argument `account_index_out` must be a non-null and valid pointer for writing. +pub type GetAccountIndexByAddressCallback = + extern "C" fn(account_address: *const u8, account_index: *mut u64) -> u8; + +/// External function for getting token account states for an account. +/// The bytes in the returned `Vec` contains binary serialized list of token indexes and token account states. +/// +/// Returns pointer to a uniquely owned [`Vec`]. The `Vec` should be allocated +/// by the callee using `copy_to_vec_ffi` in `wasm-chain-integration`. +/// The returned `Vec` must be deallocated by the caller. +/// +/// # Arguments +/// +/// - `account_index` The index of the account to update a token balance for. Must be a valid +/// account index of an existing account. +pub type GetTokenAccountStatesCallback = extern "C" fn(account_index: u64) -> *mut Vec; + +pub mod tests_helpers { + use super::*; + + pub const UNIMPLEMENTED_UPDATE_TOKEN_ACCOUNT_BALANCE: UpdateTokenAccountBalanceCallback = { + extern "C" fn fun(_: u64, _: u64, _: u64, _: u8) -> u8 { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_TOUCH_TOKEN_ACCOUNT: TouchTokenAccountCallback = { + extern "C" fn fun(_: u64, _: u64) { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_READ_TOKEN_ACCOUNT_BALANCE: ReadTokenAccountBalanceCallback = { + extern "C" fn fun(_: u64, _: u64) -> u64 { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_INCREMENT_PLT_UPDATE_SEQUENCE_NUMBER: + IncrementPltUpdateSequenceNumberCallback = { + extern "C" fn fun() { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_GET_CANONICAL_ADDRESS_BY_ACCOUNT_INDEX: + GetCanonicalAddressByAccountIndexCallback = { + extern "C" fn fun(_: u64, _: *mut u8) -> u8 { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_GET_ACCOUNT_INDEX_BY_ADDRESS: GetAccountIndexByAddressCallback = { + extern "C" fn fun(_: *const u8, _: *mut u64) -> u8 { + unimplemented!() + } + fun + }; + + pub const UNIMPLEMENTED_GET_TOKEN_ACCOUNT_STATES: GetTokenAccountStatesCallback = { + extern "C" fn fun(_: u64) -> *mut Vec { + unimplemented!() + } + fun + }; +} diff --git a/plt/plt-block-state/src/ffi/external_chain_parameters.rs b/plt/plt-block-state/src/ffi/external_chain_parameters.rs new file mode 100644 index 0000000000..90bdb2c459 --- /dev/null +++ b/plt/plt-block-state/src/ffi/external_chain_parameters.rs @@ -0,0 +1,208 @@ +//! This module provides a C ABI for external chain parameters. +//! +//! It is only available if the `ffi` feature is enabled. + +use super::status; +use crate::ffi::blob_store_callbacks::{LoadCallback, StoreCallback}; +use crate::persistent::blob_store; +use crate::persistent::blob_store::BlobStoreLocation; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::chain_parameters::PersistentChainParameters; +use crate::persistent::hash::Hashable; +use concordium_base::contracts_common::Duration; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; + +/// Allocate new external chain parameters with an initial maximum lock duration. +/// +/// # Safety +/// +/// - `params_out` must be non-null and valid for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_p11_new_external_chain_parameters( + max_lock_duration: u64, + params_out: *mut *mut PersistentChainParameters, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!params_out.is_null(), "params_out is a null pointer."); + unsafe { + *params_out = Box::into_raw(Box::new( + PersistentChainParameters::p11_new_external_chain_parameters( + Duration::from_millis(max_lock_duration), + ), + )); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Deallocate external chain parameters. +/// +/// # Safety +/// +/// - `params` must be a unique, non-null pointer to a well-formed [`PersistentChainParameters`]. +#[unsafe(no_mangle)] +extern "C" fn ffi_free_external_chain_parameters(params: *mut PersistentChainParameters) { + let panic_message = status::catch_unwind(move || { + assert!(!params.is_null(), "params is a null pointer."); + let params = unsafe { Box::from_raw(params) }; + drop(params); + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + } +} + +/// Load external chain parameters from the blob store. +/// +/// # Safety +/// +/// - `load_callback` must be a valid blob-store load callback. +/// - `params_out` must be non-null and valid for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_load_external_chain_parameters( + load_callback: LoadCallback, + blob_ref: BlobStoreLocation, + protocol_version: u64, + params_out: *mut *mut PersistentChainParameters, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!params_out.is_null(), "params_out is a null pointer."); + let protocol_version = + ProtocolVersion::try_from(protocol_version).expect("Unknown protocol version"); + let params = + PersistentChainParameters::load_from_store(&load_callback, blob_ref, protocol_version) + .expect("Failed loading external chain parameters"); + unsafe { + *params_out = Box::into_raw(Box::new(params)); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Store external chain parameters in the blob store. +/// +/// # Safety +/// +/// - `store_callback` must be a valid blob-store store callback. +/// - `blob_ref_out` must be non-null and valid for writing. +/// - `params` must be non-null and point to well-formed [`PersistentChainParameters`]. +#[unsafe(no_mangle)] +extern "C" fn ffi_store_external_chain_parameters( + mut store_callback: StoreCallback, + blob_ref_out: *mut BlobStoreLocation, + params: *const PersistentChainParameters, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!blob_ref_out.is_null(), "blob_ref_out is a null pointer."); + assert!(!params.is_null(), "params is a null pointer."); + let params = unsafe { &*params }; + let reference = blob_store::store_to_store(&mut store_callback, params); + unsafe { + *blob_ref_out = reference; + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Cache external chain parameters into memory. +/// +/// # Safety +/// +/// - `load_callback` must be a valid blob-store load callback. +/// - `params` must be non-null and point to well-formed [`PersistentChainParameters`]. +#[unsafe(no_mangle)] +extern "C" fn ffi_cache_external_chain_parameters( + load_callback: LoadCallback, + params: *const PersistentChainParameters, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!params.is_null(), "params is a null pointer."); + let params = unsafe { &*params }; + params + .cache_reference_values(&load_callback) + .expect("Failed caching external chain parameters"); + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Compute the hash of external chain parameters. +/// +/// # Safety +/// +/// - `load_callback` must be a valid blob-store load callback. +/// - `params` must be non-null and point to well-formed [`PersistentChainParameters`]. +/// - `hash_out` must be non-null and valid for writes of 32 bytes. +#[unsafe(no_mangle)] +extern "C" fn ffi_hash_external_chain_parameters( + load_callback: LoadCallback, + params: *const PersistentChainParameters, + hash_out: *mut u8, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!params.is_null(), "params is a null pointer."); + assert!(!hash_out.is_null(), "hash_out is a null pointer."); + let params = unsafe { &*params }; + let hash = params + .hash(&load_callback) + .expect("Failed hashing external chain parameters"); + unsafe { + std::ptr::copy_nonoverlapping(hash.as_ptr(), hash_out, hash.len()); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} + +/// Read `max_lock_duration` from external chain parameters. +/// +/// # Safety +/// +/// - `params` must be non-null and point to well-formed [`PersistentChainParameters`]. +/// - `duration_out` must be non-null and valid for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_get_external_chain_parameters_max_lock_duration( + params: *const PersistentChainParameters, + duration_out: *mut u64, +) -> status::FfiStatusCode { + let panic_message = status::catch_unwind(move || { + assert!(!params.is_null(), "params is a null pointer."); + assert!(!duration_out.is_null(), "duration_out is a null pointer."); + let params = unsafe { &*params }; + let duration = match params { + PersistentChainParameters::P11(params) => params.max_lock_duration, + }; + unsafe { + *duration_out = duration.millis(); + } + }); + if let Some(message) = panic_message { + eprintln!("{}", message); + status::FfiStatusCode::Panic + } else { + status::FfiStatusCode::Success + } +} diff --git a/plt/plt-block-state/src/ffi/memory.rs b/plt/plt-block-state/src/ffi/memory.rs new file mode 100644 index 0000000000..f6aa362d6a --- /dev/null +++ b/plt/plt-block-state/src/ffi/memory.rs @@ -0,0 +1,43 @@ +// todo remove or change this module as part of https://linear.app/concordium/issue/PSR-61/address-potentially-unsafe-behaviour-cased-by-using-shrink-to-fit + +use libc::size_t; + +/// Free an array that was converted to a pointer from a vector. +/// This assumes the vector's capacity and length were the same. +/// +/// # Safety +/// +/// - Argument `ptr` must be a non-null and valid unique pointer to a `Vec`. +/// - Argument `len` must be equal to the length AND capacity of the given `Vec` +#[unsafe(no_mangle)] +extern "C" fn free_array_len_2(ptr: *mut u8, len: u64) { + unsafe { + Vec::from_raw_parts(ptr, len as usize, len as usize); + } +} + +/// Allocated array together with the array length. +/// Must be freed with [`free_array_len_2`]. +pub struct ArrayWithLength { + /// Unique pointer to first byte in array. + pub array: *mut u8, + /// Length of the array + pub length: size_t, +} + +/// Allocate an array by using the given allocated `Vec` and converting it into raw parts. +/// Must be freed with [`free_array_len_2`]. +pub fn alloc_array_from_vec(mut bytes: Vec) -> ArrayWithLength { + // shrink Vec should that we know capacity and length are equal (this is important when we later free with free_array_len_2) + bytes.shrink_to_fit(); + + let (array, length, capacity) = bytes.into_raw_parts(); + + // todo for now we assert that capacity is equals to the length, but we should address that this may not be the case in a better way, see https://linear.app/concordium/issue/PSR-61/address-potentially-unsafe-behaviour-cased-by-using-shrink-to-fit + assert_eq!( + capacity, length, + "vec capacity not equal to length after call to shrink_to_fit" + ); + + ArrayWithLength { array, length } +} diff --git a/plt/plt-block-state/src/ffi/status.rs b/plt/plt-block-state/src/ffi/status.rs new file mode 100644 index 0000000000..7f04d91d12 --- /dev/null +++ b/plt/plt-block-state/src/ffi/status.rs @@ -0,0 +1,58 @@ +/// Returned status code used in FFI calls into this library. +/// +/// This must match the `FFIStatusCode` type defined on the haskell side. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum FfiStatusCode { + /// The call succeeded. + Success = 0, + /// The call failed gracefully. + Failed = 1, + /// The call resulted in a (caught) panic. + Panic = 2, +} + +/// Helper function for wrapping calls with [`std::panic::catch_unwind`] then mapping a panic to the +/// correct status code and extracting the panic message. +/// +/// # Arguments +/// +/// - `function` The closure which might panic +pub fn catch_unwind(function: F) -> Option +where + F: std::panic::UnwindSafe + FnOnce(), +{ + if let Err(err) = std::panic::catch_unwind(function) { + let message = if let Some(message) = err.downcast_ref::() { + message.clone() + } else if let Some(message) = err.downcast_ref::<&str>() { + message.to_string() + } else { + "Unknown panic reason".to_string() + }; + Some(message) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_catch_unwind_panics_str() { + let message = catch_unwind(|| { + panic!("my panic message"); + }); + assert_eq!(message, Some("my panic message".to_string())); + } + + #[test] + fn test_catch_unwind_panics_string() { + let message = catch_unwind(|| { + panic!("my panic message {:?}", vec![5]); + }); + assert_eq!(message, Some("my panic message [5]".to_string())); + } +} diff --git a/plt/plt-block-state/src/lib.rs b/plt/plt-block-state/src/lib.rs new file mode 100644 index 0000000000..d9449ec5a7 --- /dev/null +++ b/plt/plt-block-state/src/lib.rs @@ -0,0 +1,7 @@ +pub mod entity; +pub mod external; +pub mod failure; +#[cfg(feature = "ffi")] +pub mod ffi; +pub mod persistent; +pub mod utils; diff --git a/plt/plt-block-state/src/persistent.rs b/plt/plt-block-state/src/persistent.rs new file mode 100644 index 0000000000..2aefd63d61 --- /dev/null +++ b/plt/plt-block-state/src/persistent.rs @@ -0,0 +1,16 @@ +//! Persistent (immutable) model for the block state. The types in this module are the types +//! closes to the actual storage (blob store). +//! The model generally allows representing block state components and values +//! in memory, in the [blob store](super::blob_store), or both (cached), and the representation +//! may change during the lifetime of components and values (via interior mutability). + +pub mod blob_reference; +pub mod blob_store; +pub mod block_state; +pub mod cacheable; +pub mod chain_parameters; +pub mod hash; +pub mod lfmb_tree; +pub mod protocol_level_locks; +pub mod protocol_level_tokens; +pub mod smart_contract_trie; diff --git a/plt/plt-block-state/src/persistent/blob_reference.rs b/plt/plt-block-state/src/persistent/blob_reference.rs new file mode 100644 index 0000000000..6391185cf8 --- /dev/null +++ b/plt/plt-block-state/src/persistent/blob_reference.rs @@ -0,0 +1,21 @@ +//! Definition of blob store reference types. A blob store reference may largely be thought of as +//! a pointer to a value in the blob store, but the representation of the value may vary during +//! the lifetime of the reference. The value may be represented in, +//! +//! * the blob store only (the reference is a pure blob store pointer) +//! * memory (the value will eventually be written to the blob store), +//! * both the blob store and memory (the reference is a pointer +//! that also caches the value in memory). +//! +//! A blob store reference to a block state component +//! is stored in the blob store as a [blob location](super::blob_store::BlobStoreLocation) +//! This allows for +//! +//! * sharing block state components pointed to by blob references between different block states +//! * deciding when to load the referenced block state component, +//! e.g. on demand as needed or via [`Cacheable`](super::cacheable::Cacheable) +//! +//! Blob store reference types are also cheaply clonable, such that they can be shared in the +//! in-memory representation of the block state. + +pub mod hashed_cacheable_reference; diff --git a/plt/plt-block-state/src/persistent/blob_reference/hashed_cacheable_reference.rs b/plt/plt-block-state/src/persistent/blob_reference/hashed_cacheable_reference.rs new file mode 100644 index 0000000000..2927bc1c29 --- /dev/null +++ b/plt/plt-block-state/src/persistent/blob_reference/hashed_cacheable_reference.rs @@ -0,0 +1,655 @@ +//! Representation of an immutable, cacheable and lazily hashed value of type `V`. +//! +//! See [`HashedCacheableRef`]. + +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreLocation, BlobStoreMovable, BlobStoreStore, Loadable, ParseResultExt, + Storable, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use crate::utils::Cow; +use concordium_base::common::{Buffer, Get, Put}; +use concordium_base::hashes::Hash; +use std::io::Read; +use std::sync::{Arc, OnceLock}; + +/// Representation of an immutable, cachable and lazily hashed value of type `V`. +/// The represented value is immutable in the sense that the value itself does not change, +/// once the [`HashedCacheableRef`] has been created. The value representation can be in +/// +/// * memory: initial representation for a new value created with [`HashedCacheableRef::new`] +/// * blob store: initial representation for a value loaded +/// from the blob store with [`Loadable::load_from_buffer`] +/// * cached: representation where the value is both in the blob store and in memory +/// +/// The cached representation is the result of either storing a value represented in +/// memory with [`Storable::store_to_buffer`] or caching a value in the blob store +/// with [`Cacheable::cache_reference_values`]. +/// +/// ## Interior mutability +/// +/// The representation change during the lifetime of [`HashedCacheableRef`] is implemented +/// via interior mutability, but the represented value itself never changes during the lifetime. +/// +/// ## Hashing +/// +/// The hash of the represented value is calculated lazily when needed, and cached +/// via interior mutability. +#[derive(Debug)] +pub struct HashedCacheableRef { + /// The representation is wrapped in a [`Arc`] to allow cheap cloning and + /// interior mutability of the shared inner value. + inner: Arc>, +} + +impl Default for HashedCacheableRef { + fn default() -> Self { + Self::new(Default::default()) + } +} + +impl HashedCacheableRef { + /// Create a new value represented in memory. + pub fn new(value: V) -> Self { + let inner = HashedBufferedRefInner { + hash: OnceLock::new(), + repr: HashedCacheableRefRepr::Memory { + value, + blob_location_lock: OnceLock::new(), + }, + }; + + Self { + inner: Arc::new(inner), + } + } + + /// Return the referenced value. If the value is already in memory, the value + /// is returned as borrowed. If it is not in memory, it is loaded from the + /// blob store, and returned as owned. + /// + /// Loading from the blob store will not make the value cached in the reference. + /// + /// # Errors + /// + /// Returns [`BlockStateFailure`] if decoding data from the blob store fails. + pub fn value(&self, loader: &impl BlobStoreLoad) -> BlockStateResult> + where + V: Loadable, + { + self.inner.repr.get_or_load_value(loader) + } +} + +impl<'b, V> Cow<'b, HashedCacheableRef> { + /// Return the referenced value (as implemented by [`HashedCacheableRef::value`]) for an + /// owned or borrowed reference. If the reference is `Owned`, and + /// [`HashedCacheableRef::value`] returns a borrowed value, `bind_value` returns + /// [`BlockStateFailure::CowJoin`]. + /// Notice that an owned or borrowed reference that has just been returned by calling + /// [`HashedCacheableRef::value`], will never return [`BlockStateFailure::CowJoin`] + /// when calling `bind_value` on it. + /// + /// This function is essentially binding the operation + /// [`HashedCacheableRef::value`] in the monadic structure of [`Cow`]. + /// But [`Cow`] is not fully monadic, `Owned(Borrowed(val))` cannot be "joined" + /// into neither `Owned` nor `Borrowed`, hence `bind_value` will return an error in that case. + /// Notice that all other combinations of `Owned` and `Borrowed` can be "joined". + pub fn bind_value( + self, + loader: &impl BlobStoreLoad, + context: &'static str, + ) -> BlockStateResult> + where + V: Loadable, + { + Ok(match self { + Cow::Owned(hcr) => match hcr.value(loader)? { + Cow::Owned(val) => Cow::Owned(val), + Cow::Borrowed(_) => { + return Err(BlockStateFailure::CowJoin(context)); + } + }, + Cow::Borrowed(hcr) => hcr.value(loader)?, + }) + } +} + +/// Implement [`Clone`] explicitly, such that clonability does not depend on +/// if `V` is clonable. +impl Clone for HashedCacheableRef { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +/// The [`HashedCacheableRef`] behind the `Arc`. +#[derive(Debug)] +struct HashedBufferedRefInner { + /// Lazily calculated hash. If set, it is the hash of the referenced value. + hash: OnceLock, + /// Representation of the value. + repr: HashedCacheableRefRepr, +} + +/// The possible representations of the referenced value. +/// +/// Notice that the two variants decide where the value was represented, when the reference +/// was created (in blob store or in memory). But both of the variants `Store` and `Memory` +/// can represent a value that is in memory and stored at the same time. +#[derive(Debug)] +enum HashedCacheableRefRepr { + /// The value is in the blob store, and is maybe cached. + Store { + /// Location of the value in the blob store + blob_location: BlobStoreLocation, + /// In-memory value. Is set if the value is currently cached in memory. + value_lock: OnceLock, + }, + /// The value is in memory, and is maybe also stored (the same as cached) + Memory { + /// In-memory value. + value: V, + /// Location of the value in the blob store. Is set if the value is also stored in the blob store. + blob_location_lock: OnceLock, + }, +} + +impl HashedCacheableRefRepr { + /// Load the referenced value and return it. If the value is already in memory, a reference + /// to it is simply returned. If it is not in memory, it is loaded from the blob store, + /// and returned as owned. + /// + /// Loading from the blob store will not make the value cached in the reference. + fn get_or_load_value(&self, loader: &impl BlobStoreLoad) -> BlockStateResult> + where + V: Loadable, + { + Ok(match self { + HashedCacheableRefRepr::Store { + blob_location, + value_lock, + } => match value_lock.get() { + None => { + let value: V = blob_store::load_from_store(loader, *blob_location)?; + Cow::Owned(value) + } + Some(value) => Cow::Borrowed(value), + }, + HashedCacheableRefRepr::Memory { value, .. } => Cow::Borrowed(value), + }) + } + + /// Cache the referenced value and return it. If the value is already in memory, a reference + /// to it is simply returned. If it is not in memory, it is loaded from the blob store and + /// cached first. + fn get_or_cache_value(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<&V> + where + V: Loadable, + { + Ok(match self { + HashedCacheableRefRepr::Store { + blob_location, + value_lock, + } => { + // When OnceLock::get_mut_or_try_init is stable, we can use that instead of the + // get/set pattern used here. get_mut_or_try_init imposes a critical region such + // that two threads will not load the value for caching. Right now we just log + // if it happens. + match value_lock.get() { + None => { + let value: V = blob_store::load_from_store(loader, *blob_location)?; + value_lock + .set(value) + .inspect_err(|_| eprintln!("HashedCacheableRef: Value loaded by two threads at the same time")) + .ok(); + value_lock.get().expect("HashedCacheableRefRepr::get_or_cache_value: value not present though just set in lock") + } + Some(value) => value, + } + } + HashedCacheableRefRepr::Memory { value, .. } => value, + }) + } + + /// Store the value and return its [`BlobStoreLocation`]. If the value is already stored in + /// the blob store, the [`BlobStoreLocation`] for it is simply returned. If it is not stored, + /// it is stored into the blob store, and the resulting [`BlobStoreLocation`] is saved in + /// [`HashedCacheableRefRepr::Cache`] and returned. + fn get_reference_or_store(&self, storer: &mut impl BlobStoreStore) -> BlobStoreLocation + where + V: Storable, + { + match self { + HashedCacheableRefRepr::Store { blob_location, .. } => *blob_location, + HashedCacheableRefRepr::Memory { + value, + blob_location_lock, + } => match blob_location_lock.get() { + Some(blob_location) => *blob_location, + None => { + *blob_location_lock.get_or_init(|| blob_store::store_to_store(storer, value)) + } + }, + } + } +} + +impl Loadable for HashedCacheableRef { + fn load_from_buffer( + mut buffer: impl Read, + _loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + let blob_location: BlobStoreLocation = buffer.get().map_parse_err_to_block_state_err()?; + let inner = HashedBufferedRefInner { + hash: OnceLock::new(), + repr: HashedCacheableRefRepr::Store { + blob_location, + value_lock: OnceLock::new(), + }, + }; + + Ok(Self { + inner: Arc::new(inner), + }) + } +} + +impl Storable for HashedCacheableRef { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + let reference = self.inner.repr.get_reference_or_store(storer); + buffer.put(reference); + } +} + +impl Cacheable for HashedCacheableRef { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + let value = self.inner.repr.get_or_cache_value(loader)?; + value.cache_reference_values(loader) + } +} + +impl Hashable for HashedCacheableRef { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + // When OnceLock::get_mut_or_try_init is stable, we can use that instead of the + // get/set pattern used here. get_mut_or_try_init imposes a critical region such + // that two threads will not hash the value. Right now we just log + // if it happens. + match self.inner.hash.get() { + Some(hash) => Ok(*hash), + None => { + let value = self.inner.repr.get_or_load_value(loader)?; + let hash = value.hash(loader)?; + self.inner + .hash + .set(hash) + .inspect_err(|_| { + eprintln!( + "HashedCacheableRef: Hash calculated by two threads at the same time" + ) + }) + .ok(); + Ok(hash) + } + } + } +} + +impl BlobStoreMovable for HashedCacheableRef { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let migrated_value = self + .value(from_loader)? + .move_blob_store(from_loader, to_storer)?; + let new_hcr = HashedCacheableRef::new(migrated_value); + // Eagerly store value in the new store + new_hcr.inner.repr.get_reference_or_store(to_storer); + Ok(new_hcr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::persistent::blob_store::StoreSerialized; + use crate::persistent::blob_store::test_stub::{BlobStoreStub, UnreachableBlobStore}; + use assert_matches::assert_matches; + use std::fmt::Debug; + + type TestRef = HashedCacheableRef>; + + fn assert_in_memory_repr(hcr: &HashedCacheableRef) -> &V { + assert_matches!(&hcr.inner.repr, HashedCacheableRefRepr::Memory { blob_location_lock, value } => { + assert!(blob_location_lock.get().is_none(), "in blob store"); + value + }) + } + + fn assert_stored_repr(hcr: &HashedCacheableRef) -> BlobStoreLocation { + assert_matches!(&hcr.inner.repr, HashedCacheableRefRepr::Store { blob_location, value_lock } => { + assert!(value_lock.get().is_none(), "in memory"); + *blob_location + }) + } + + fn assert_cached_repr(hcr: &HashedCacheableRef) -> (&V, BlobStoreLocation) { + match &hcr.inner.repr { + HashedCacheableRefRepr::Store { + value_lock, + blob_location, + } => (value_lock.get().expect("not in memory"), *blob_location), + HashedCacheableRefRepr::Memory { + value, + blob_location_lock, + } => (value, *blob_location_lock.get().expect("not in blob store")), + } + } + + /// Test full lifecycle of a value: + /// + /// * create as new in memory + /// * store the in-memory value to blob store + /// * load the value from the blob store + /// * cache the stored value + /// + /// There are further test cases of store and cache applied on representations + /// not covered in this test. + #[test] + fn test_store_load_and_cache() { + let mut store = BlobStoreStub::default(); + + // Create new value an assert representation is memory + let val1 = TestRef::new(StoreSerialized(1u64)); + assert_eq!(*assert_in_memory_repr(&val1), StoreSerialized(1)); + + // Store value to blob store and assert representation is now cache. + let blob_ref = blob_store::store_to_store(&mut store, &val1); + let (val_tmp, val_ref) = assert_cached_repr(&val1); + assert_eq!(*val_tmp, StoreSerialized(1)); + + drop(val1); + + // Load value from blob store and assert representation is store. + let val2: TestRef = blob_store::load_from_store(&store, blob_ref).unwrap(); + assert_eq!(assert_stored_repr(&val2), val_ref); + + // Cache the value and assert representation is now cache + val2.cache_reference_values(&store).expect("cache"); + let (val_tmp, val_ref_tmp) = assert_cached_repr(&val2); + assert_eq!(*val_tmp, StoreSerialized(1)); + assert_eq!(val_ref_tmp, val_ref); + } + + /// Test move (migrate) a reference a value from one blob store to another: + /// + /// * create value in source blob store + /// * migrate to new blob store + /// * load the new value from the new blob store + #[test] + fn test_move_blob_store() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create new value and store it in the source blob store + let val = TestRef::new(StoreSerialized(1u64)); + blob_store::store_to_store(&mut from_store, &val); + + // Move to destination blob store + let new_val = val.move_blob_store(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + drop(val); + + // Assert moved reference + assert_cached_repr(&new_val); + assert_eq!(*new_val.value(&to_store).unwrap(), StoreSerialized(1)); + drop(new_val); + + // Load moved reference and assert + let new_val2: TestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + assert_eq!(*new_val2.value(&to_store).unwrap(), StoreSerialized(1)); + } + + /// Test storing cached value. + #[test] + fn test_store_cached_value() { + let mut store = BlobStoreStub::default(); + let val1 = TestRef::new(StoreSerialized(1u64)); + + // Store value to make it cached + blob_store::store_to_store(&mut store, &val1); + let (_, val_ref) = assert_cached_repr(&val1); + + // Store value again and assert this does not change the reference to the value. + blob_store::store_to_store(&mut store, &val1); + let (val_tmp, val_ref_tmp) = assert_cached_repr(&val1); + assert_eq!(*val_tmp, StoreSerialized(1)); + assert_eq!(val_ref_tmp, val_ref); + } + + /// Test storing stored value. + #[test] + fn test_store_stored_value() { + let mut store = BlobStoreStub::default(); + let val1 = TestRef::new(StoreSerialized(1u64)); + let blob_ref = blob_store::store_to_store(&mut store, &val1); + drop(val1); + + // Load value to make it stored + let val2: TestRef = blob_store::load_from_store(&store, blob_ref).unwrap(); + let val_ref = assert_stored_repr(&val2); + + // Store value and assert this does not change the reference to the value. + blob_store::store_to_store(&mut store, &val2); + assert_eq!(assert_stored_repr(&val2), val_ref); + } + + /// Test caching cached value. + #[test] + fn test_cache_cached_value() { + let mut store = BlobStoreStub::default(); + let val1 = TestRef::new(StoreSerialized(1u64)); + + // Store value to make it cached + blob_store::store_to_store(&mut store, &val1); + let (val_tmp, val_ref) = assert_cached_repr(&val1); + assert_eq!(*val_tmp, StoreSerialized(1)); + + // Cache value and assert it does not change representation + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + val1.cache_reference_values(&UnreachableBlobStore) + .expect("cache"); + let (val_tmp, val_ref_tmp) = assert_cached_repr(&val1); + assert_eq!(*val_tmp, StoreSerialized(1)); + assert_eq!(val_ref_tmp, val_ref); + } + + /// Test caching in-memory value. + #[test] + fn test_cache_in_memory_value() { + let val1 = TestRef::new(StoreSerialized(1u64)); + + // Cache value and assert it does not change representation + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + val1.cache_reference_values(&UnreachableBlobStore) + .expect("cache"); + assert_eq!(*assert_in_memory_repr(&val1), StoreSerialized(1)); + } + + /// Test [`HashedCacheableRef::value`] + #[test] + fn value() { + let mut store = BlobStoreStub::default(); + + // Test in-memory value. Assert in-memory representation does not change. + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + let val1 = TestRef::new(StoreSerialized(1u64)); + assert_eq!( + *val1.value(&UnreachableBlobStore).unwrap(), + StoreSerialized(1u64) + ); + assert_in_memory_repr(&val1); + + // Store value to make it cached + let blob_ref = blob_store::store_to_store(&mut store, &val1); + assert_cached_repr(&val1); + + // Test cached value. Assert cached representation does not change. + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + assert_eq!( + *val1.value(&UnreachableBlobStore).unwrap(), + StoreSerialized(1u64) + ); + assert_cached_repr(&val1); + + // Load value to make it stored. + drop(val1); + let val2: TestRef = blob_store::load_from_store(&store, blob_ref).unwrap(); + assert_stored_repr(&val2); + + // Test stored value. Assert stored representation does not change. + assert_eq!(*val2.value(&store).unwrap(), StoreSerialized(1u64)); + assert_stored_repr(&val2); + } + + /// Test hash in-memory value. + #[test] + fn test_hash_in_memory_value() { + let store = BlobStoreStub::default(); + + // Test hash in-memory value. + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + let val1 = TestRef::new(StoreSerialized(1u64)); + assert_eq!(val1.inner.hash.get(), None); + assert_eq!( + val1.hash(&UnreachableBlobStore).unwrap(), + StoreSerialized(1u64).hash(&store).unwrap() + ); + assert_eq!( + val1.inner.hash.get().copied(), + Some(StoreSerialized(1u64).hash(&store).unwrap()) + ); + } + + /// Test hash cached value. + #[test] + fn test_hash_cached_value() { + let mut store = BlobStoreStub::default(); + + // Store value to make it cached + let val1 = TestRef::new(StoreSerialized(1u64)); + blob_store::store_to_store(&mut store, &val1); + assert_cached_repr(&val1); + + // Test hash cached value. + // Assert that we don't need to read from the blob store by using UnreachableBlobStore + assert_eq!(val1.inner.hash.get(), None); + assert_eq!( + val1.hash(&UnreachableBlobStore).unwrap(), + StoreSerialized(1u64).hash(&store).unwrap() + ); + assert_eq!( + val1.inner.hash.get().copied(), + Some(StoreSerialized(1u64).hash(&store).unwrap()) + ); + } + + /// Test hash stored value. + #[test] + fn test_hash_stored_value() { + let mut store = BlobStoreStub::default(); + + // Load value to make it stored + let val1 = TestRef::new(StoreSerialized(1u64)); + let blob_ref = blob_store::store_to_store(&mut store, &val1); + drop(val1); + let val2: TestRef = blob_store::load_from_store(&store, blob_ref).unwrap(); + assert_stored_repr(&val2); + + // Test hash stored value. Assert stored representation does not change. + assert_eq!(val2.inner.hash.get(), None); + assert_eq!( + val2.hash(&store).unwrap(), + StoreSerialized(1u64).hash(&store).unwrap() + ); + assert_eq!( + val2.inner.hash.get().copied(), + Some(StoreSerialized(1u64).hash(&store).unwrap()) + ); + assert_stored_repr(&val2); + + // Test hash again. This time we don't need to read from blob store, since we cache the hash. + assert_eq!( + val2.hash(&UnreachableBlobStore).unwrap(), + StoreSerialized(1u64).hash(&store).unwrap() + ); + } + + type NestedTestRef = HashedCacheableRef>>; + + /// Test store, load and cache a reference with a nested reference. + #[test] + fn test_nested_reference_store_load_and_cache() { + let mut store = BlobStoreStub::default(); + let val1 = HashedCacheableRef::new(HashedCacheableRef::new(StoreSerialized(1u64))); + + // Store value to blob store and assert representation is now cache. + let blob_ref = blob_store::store_to_store(&mut store, &val1); + let (val_nested1, val_blob_ref) = assert_cached_repr(&val1); + let (_, val_nested_blob_ref) = assert_cached_repr(val_nested1); + + drop(val1); + + // Load value from blob store and assert representation is store. + let val2: NestedTestRef = blob_store::load_from_store(&store, blob_ref).unwrap(); + assert_eq!(assert_stored_repr(&val2), val_blob_ref); + + // Cache the value and assert representation is now cache + val2.cache_reference_values(&store).expect("cache"); + let (val_nested2, val_ref_tmp) = assert_cached_repr(&val2); + assert_eq!(val_ref_tmp, val_blob_ref); + let (val_tmp, val_ref_tmp) = assert_cached_repr(val_nested2); + assert_eq!(*val_tmp, StoreSerialized(1)); + assert_eq!(val_ref_tmp, val_nested_blob_ref); + } + + /// Test migrate a reference with a nested reference. + #[test] + fn test_nested_reference_migrate() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create new value and store it in the source blob store + let val = HashedCacheableRef::new(HashedCacheableRef::new(StoreSerialized(1u64))); + blob_store::store_to_store(&mut from_store, &val); + + // Migrate to destination blob store + let new_val = val.move_blob_store(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + drop(val); + + // Assert migrated reference + assert_eq!( + *new_val.value(&to_store).unwrap().value(&to_store).unwrap(), + StoreSerialized(1) + ); + drop(new_val); + + // Load migrated reference and assert + let new_val2: NestedTestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + assert_eq!( + *new_val2.value(&to_store).unwrap().value(&to_store).unwrap(), + StoreSerialized(1) + ); + } +} diff --git a/plt/plt-block-state/src/persistent/blob_store.rs b/plt/plt-block-state/src/persistent/blob_store.rs new file mode 100644 index 0000000000..f4602d6fba --- /dev/null +++ b/plt/plt-block-state/src/persistent/blob_store.rs @@ -0,0 +1,311 @@ +//! Definition of the blob store interface via [`BlobStoreLoad`] and [`BlobStoreStore`]. +//! The blob store is a file system based storage, that stores the state to a flat +//! binary file (per protocol version) that is only appended to. +//! +//! The module also defines the traits [`Loadable`] and [`Storable`] that block state components +//! must implement to be storable in the blob store. + +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::utils::Cow; +use concordium_base::common; +use concordium_base::common::{Buffer, Deserial, Get, Put, Serial, Serialize}; +use std::any; +use std::io::Read; + +/// Location of a value in the blob store. +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Serialize)] +#[repr(transparent)] +pub struct BlobStoreLocation(pub u64); + +/// Trait implemented by types that can be used to store binary data, and return +/// a handle for loading data. Dual to [`BlobStoreLoad`]. +pub trait BlobStoreStore { + /// Store the provided value and return a reference that can be used + /// to load it. + fn store_raw(&mut self, data: impl AsRef<[u8]>) -> BlobStoreLocation; +} + +impl BlobStoreStore for &mut T { + fn store_raw(&mut self, data: impl AsRef<[u8]>) -> BlobStoreLocation { + (**self).store_raw(data) + } +} + +/// Trait implemented by types that can load data from given locations. +/// Dual to [`BlobStoreStore`]. +pub trait BlobStoreLoad { + /// Load the provided value from the given location. The implementation of + /// this should match [BlobStoreStore::store_raw]. + fn load_raw(&self, location: BlobStoreLocation) -> Vec; +} + +impl BlobStoreLoad for &T { + fn load_raw(&self, location: BlobStoreLocation) -> Vec { + (**self).load_raw(location) + } +} + +impl BlobStoreLoad for &mut T { + fn load_raw(&self, location: BlobStoreLocation) -> Vec { + (**self).load_raw(location) + } +} + +/// A trait implemented by types that can be loaded from a [blob store](BlobStoreLoad). +pub trait Loadable: Sized { + /// Load value from the bytes in the given `buffer` that has been retrieved from the blob store. + /// If the value is composed of [blob references](super::blob_reference), these references should not + /// have their values loaded into memory as a result of the [`Self::load_from_buffer`] operation. + /// As such, [`Self::load_from_buffer`] is a "shallow" operation. + /// + /// The given `loader` should generally not be used. If it is needed, it is generally a warning + /// sign that the state might not have the right model. + /// + /// To load a value from a given [`BlobStoreLocation`], use [`load_from_store`]. + fn load_from_buffer( + buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result; +} + +impl Loadable for Option { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let tag: u8 = buffer.get().map_parse_err_to_block_state_err()?; + match tag { + 0 => Ok(None), + 1 => Ok(Some(Loadable::load_from_buffer(buffer, loader)?)), + _ => Err(BlockStateFailure::BlobStoreDecode(format!( + "Invalid option tag: {tag}" + ))), + } + } +} + +/// A trait implemented by types that can be stored to a [blob store](BlobStoreStore). +pub trait Storable { + /// Store the value in the given `buffer` that will be written to the blob store. + /// Notice that when storing the value, the operation must recursively store all + /// values pointed to by the [blob references](super::blob_reference) the value may be composed of, + /// if these values are not already represented in the blob store. + /// As such, `store` is a "deep" operation. + /// + /// To store a value to a given [`BlobStoreLocation`], use [`load_from_store`]. + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore); +} + +impl Storable for &T { + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + (**self).store_to_buffer(buffer, storer) + } +} + +impl Storable for &mut T { + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + (**self).store_to_buffer(buffer, storer) + } +} + +impl Storable for Option { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + match self { + None => buffer.put(0u8), + Some(inner) => { + buffer.put(1u8); + inner.store_to_buffer(buffer, storer) + } + } + } +} +/// Adapter for types implementing [`Serialize`] that +/// allows them to be used as block state components. +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)] +pub struct StoreSerialized(pub T); + +impl<'b, T> Cow<'b, StoreSerialized> { + /// Move [`Cow`] wrapped value. + pub fn cow_project(self) -> Cow<'b, T> { + match self { + Cow::Owned(this) => Cow::Owned(this.0), + Cow::Borrowed(this) => Cow::Borrowed(&this.0), + } + } +} + +impl Loadable for StoreSerialized { + fn load_from_buffer( + mut buffer: impl Read, + _loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + Ok(StoreSerialized( + buffer.get().map_parse_err_to_block_state_err()?, + )) + } +} + +impl Storable for StoreSerialized { + fn store_to_buffer(&self, mut buffer: impl Buffer, _storer: &mut impl BlobStoreStore) { + buffer.put(&self.0); + } +} + +/// Load value from the blob store at the given `location`. The value will +/// not recursively load values pointed to by [blob references](super::blob_reference) +/// is may be composed of as part of this operation. +pub fn load_from_store( + loader: &impl BlobStoreLoad, + location: BlobStoreLocation, +) -> BlockStateResult { + let bytes = loader.load_raw(location); + let mut bytes_slice = bytes.as_slice(); + let value = T::load_from_buffer(&mut bytes_slice, loader)?; + if !bytes_slice.is_empty() { + return Err(BlockStateFailure::BlobStoreDecode(format!( + "Bytes remaining after loading value of type {} from blob store", + any::type_name::() + ))); + }; + Ok(value) +} + +/// Store the value in the blob store, and return the location of the stored value. +/// Notice that when storing the value, it will recursively store all values pointed to by +/// [blob references](super::blob_reference) it may be composed of, if these values are not already +/// stored in the blob store. +pub fn store_to_store( + storer: &mut impl BlobStoreStore, + storable: impl Storable, +) -> BlobStoreLocation { + let mut buffer = Vec::new(); + storable.store_to_buffer(&mut buffer, storer); + storer.store_raw(buffer) +} + +/// Extension trait for [`common::ParseResult`] that allows mapping error type +/// to [`BlockStateFailure`]. +pub trait ParseResultExt { + /// Map the error type of [`common::ParseResult`] to [`BlockStateFailure`] + fn map_parse_err_to_block_state_err(self) -> Result; +} + +impl ParseResultExt for common::ParseResult { + fn map_parse_err_to_block_state_err(self) -> Result { + self.map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Error parsing bytes for value of type {} loaded from blob store: {}", + any::type_name::(), + err + )) + }) + } +} + +/// Trait implemented by persistent block state types to support migration when protocol version increments. +/// Such a migration moves the block state to a new blob store, hence the name of the trait. +/// Moving to a new blob store must recursively store all [blob references](super::blob_reference) +/// into the new blob store we migrate to (the blob store of the new protocol version). +/// +/// Since each protocol version has its own blob store, moving the block state is always needed at +/// protocol update, even if the block state value has no data model changes. Any changes to the data +/// model are handled at a higher level than the present trait. The present trait only implements a +/// 1-1 move of the type implementing it. +pub trait BlobStoreMovable { + /// Move the value from the blob store it is currently stored in + /// (`from_store`), to the new blob store for the next protocol version (`to_store`). + /// Notice that the value will still be persisted in the current store, it should not + /// be removed from the store. + /// + /// Moving the value must recursively move all [blob references](super::blob_reference) the + /// value is composed of to the new blob store, including storing the referenced values in the + /// new blob store. + /// The function returns the new, moved value, that represents the value on the new store, + /// and whose [blob references](super::blob_reference) points to the new blob store. + /// + /// # Arguments + /// + /// - `from_store`: loader for the blob store that the value is currently stored in + /// (the blob store we migrate from) + /// - `to_store`: storer for the blob store that we migrate to + fn move_blob_store( + &self, + from_store: &impl BlobStoreLoad, + to_store: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized; +} + +impl BlobStoreMovable for StoreSerialized { + fn move_blob_store( + &self, + _from_loader: &impl BlobStoreLoad, + _to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult { + Ok(self.clone()) + } +} + +/// Blob store stubs to be used in tests. +pub mod test_stub { + use super::*; + + /// In-memory blob store stub implemented via a `Vec`. + #[derive(Default, Debug, Clone)] + pub struct BlobStoreStub(pub Vec); + + impl BlobStoreStore for BlobStoreStub { + fn store_raw(&mut self, data: impl AsRef<[u8]>) -> BlobStoreLocation { + let data = data.as_ref(); + let reference = BlobStoreLocation(self.0.len() as u64); + self.0.put(data.len() as u64); + self.0.extend_from_slice(data); + reference + } + } + + impl BlobStoreLoad for BlobStoreStub { + fn load_raw(&self, location: BlobStoreLocation) -> Vec { + let mut source = self.0.get(location.0 as usize..).unwrap_or_else(|| { + panic!( + "no bytes at given location in BlobStoreStub: {:?}", + location + ) + }); + + let length = + Get::::get(&mut source).expect("read length from BlobStoreStub") as usize; + + source + .get(..length) + .expect("read data from BlobStoreStub") + .to_vec() + } + } + + #[test] + fn test_blob_store_stub() { + let mut store = BlobStoreStub::default(); + let ref1 = store.store_raw([1, 2, 3]); + let ref2 = store.store_raw([4, 5]); + assert_eq!(store.load_raw(ref1), vec![1, 2, 3]); + assert_eq!(store.load_raw(ref2), vec![4, 5]); + } + + /// Blob store implementation that panics when read from or written to. + #[derive(Default, Debug)] + pub struct UnreachableBlobStore; + + impl BlobStoreStore for UnreachableBlobStore { + fn store_raw(&mut self, _data: impl AsRef<[u8]>) -> BlobStoreLocation { + unreachable!("UnreachableBlobStore") + } + } + + impl BlobStoreLoad for UnreachableBlobStore { + fn load_raw(&self, _location: BlobStoreLocation) -> Vec { + unreachable!("UnreachableBlobStore") + } + } +} diff --git a/plt/plt-block-state/src/persistent/block_state.rs b/plt/plt-block-state/src/persistent/block_state.rs new file mode 100644 index 0000000000..51fd8b2f47 --- /dev/null +++ b/plt/plt-block-state/src/persistent/block_state.rs @@ -0,0 +1,100 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreLocation, BlobStoreStore, Loadable, Storable, +}; +use crate::persistent::block_state::p9::PersistentBlockStateP9; +use crate::persistent::block_state::p10::PersistentBlockStateP10; +use crate::persistent::block_state::p11::PersistentBlockStateP11; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use concordium_base::common::Buffer; +use concordium_base::hashes::Hash; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; +use std::any; +use std::io::Read; + +pub mod p10; +pub mod p11; +pub mod p9; + +/// Persistent block that that may represent any protocol version know to the Rust scheduler. +#[derive(Debug, Clone)] +pub enum PersistentBlockState { + P9(PersistentBlockStateP9), + P10(PersistentBlockStateP10), + P11(PersistentBlockStateP11), +} + +impl PersistentBlockState { + /// Construct an empty block state for the given protocol version. + pub fn empty(protocol_version: ProtocolVersion) -> Self { + match protocol_version { + ProtocolVersion::P9 => Self::P9(Default::default()), + ProtocolVersion::P10 => Self::P10(Default::default()), + ProtocolVersion::P11 => Self::P11(Default::default()), + } + } + + /// See [`blob_store::load_from_store`]. This function only differs by taking + /// protocol version as argument. + pub fn load_from_store( + loader: &impl BlobStoreLoad, + location: BlobStoreLocation, + protocol_version: ProtocolVersion, + ) -> BlockStateResult { + let bytes = loader.load_raw(location); + let mut bytes_slice = bytes.as_slice(); + let value = Self::load_from_buffer(&mut bytes_slice, loader, protocol_version)?; + if !bytes_slice.is_empty() { + return Err(BlockStateFailure::BlobStoreDecode(format!( + "Bytes remaining after loading value of type {} from blob store", + any::type_name::() + ))); + }; + Ok(value) + } + + /// See [`Loadable::load_from_buffer`]. This function only differs by taking + /// protocol version as argument. + fn load_from_buffer( + buffer: impl Read, + loader: &impl BlobStoreLoad, + protocol_version: ProtocolVersion, + ) -> BlockStateResult { + Ok(match protocol_version { + ProtocolVersion::P9 => Self::P9(Loadable::load_from_buffer(buffer, loader)?), + ProtocolVersion::P10 => Self::P10(Loadable::load_from_buffer(buffer, loader)?), + ProtocolVersion::P11 => Self::P11(Loadable::load_from_buffer(buffer, loader)?), + }) + } +} + +impl Storable for PersistentBlockState { + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + match self { + PersistentBlockState::P9(bs) => bs.store_to_buffer(buffer, storer), + PersistentBlockState::P10(bs) => bs.store_to_buffer(buffer, storer), + PersistentBlockState::P11(bs) => bs.store_to_buffer(buffer, storer), + } + } +} + +impl Cacheable for PersistentBlockState { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + match self { + PersistentBlockState::P9(bs) => bs.cache_reference_values(loader), + PersistentBlockState::P10(bs) => bs.cache_reference_values(loader), + PersistentBlockState::P11(bs) => bs.cache_reference_values(loader), + } + } +} + +impl Hashable for PersistentBlockState { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + match self { + PersistentBlockState::P9(bs) => bs.hash(loader), + PersistentBlockState::P10(bs) => bs.hash(loader), + PersistentBlockState::P11(bs) => bs.hash(loader), + } + } +} diff --git a/plt/plt-block-state/src/persistent/block_state/p10.rs b/plt/plt-block-state/src/persistent/block_state/p10.rs new file mode 100644 index 0000000000..ab9e13c1f4 --- /dev/null +++ b/plt/plt-block-state/src/persistent/block_state/p10.rs @@ -0,0 +1,4 @@ +use crate::persistent::block_state::p9::PersistentBlockStateP9; + +/// P10 block state. +pub type PersistentBlockStateP10 = PersistentBlockStateP9; diff --git a/plt/plt-block-state/src/persistent/block_state/p11.rs b/plt/plt-block-state/src/persistent/block_state/p11.rs new file mode 100644 index 0000000000..92e9514c42 --- /dev/null +++ b/plt/plt-block-state/src/persistent/block_state/p11.rs @@ -0,0 +1,325 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreStore, Loadable, Storable}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash; +use crate::persistent::hash::Hashable; +use crate::persistent::protocol_level_locks::p11::PersistentLocksP11; +use crate::persistent::protocol_level_tokens::p9::PersistentTokensP9; +use concordium_base::common::Buffer; +use concordium_base::hashes::Hash; +use std::io::Read; + +/// P11 block state. +#[derive(Debug, Clone, Default)] +pub struct PersistentBlockStateP11 { + /// Protocol-level tokens + pub(crate) tokens: HashedCacheableRef, + /// Protocol-level locks + pub(crate) locks: HashedCacheableRef, +} + +impl Loadable for PersistentBlockStateP11 { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let tokens = Loadable::load_from_buffer(&mut buffer, loader)?; + let locks = Loadable::load_from_buffer(&mut buffer, loader)?; + + Ok(Self { tokens, locks }) + } +} + +impl Storable for PersistentBlockStateP11 { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.tokens.store_to_buffer(&mut buffer, storer); + self.locks.store_to_buffer(&mut buffer, storer); + } +} + +impl Cacheable for PersistentBlockStateP11 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.tokens.cache_reference_values(loader)?; + self.locks.cache_reference_values(loader)?; + Ok(()) + } +} + +impl Hashable for PersistentBlockStateP11 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + let tokens = self.tokens.hash(loader)?; + let locks = self.locks.hash(loader)?; + + Ok(hash::hash_of_hashes(tokens, locks)) + } +} + +#[cfg(test)] +mod test { + use crate::entity::block_state::p11::BlockStateP11; + use crate::entity::entity_test_stub; + use crate::persistent::blob_store; + use crate::persistent::block_state::p11::PersistentBlockStateP11; + use crate::persistent::hash::Hashable; + use crate::persistent::protocol_level_locks::p11::{ + LockConfiguration, LockControllerConfig, LockControllerSimpleV0, + LockControllerSimpleV0Grant, LockRecipients, + }; + use crate::persistent::protocol_level_tokens::p9::{TokenConfiguration, TokenIndex}; + use concordium_base::base::AccountIndex; + use concordium_base::common::types::TransactionTime; + use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; + use concordium_base::protocol_level_tokens::{CborMemo, TokenModuleRef}; + use concordium_base::transactions::Memo; + use plt_scheduler_types::types::tokens::RawTokenAmount; + + // Store state with PLLs to blob store and load it again. + #[test] + fn test_store_and_load_locks() { + let mut context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create locks + let lock_id1 = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration1 = LockConfiguration { + lock_id: lock_id1.clone(), + recipients: LockRecipients::from(vec![AccountIndex::from(1), AccountIndex::from(2)]), + expiry: TransactionTime::from(100u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1), + roles: vec![ + LockControllerSimpleV0Capability::Cancel, + LockControllerSimpleV0Capability::Fund, + ], + }], + tokens: vec!["tokenid1".parse().unwrap(), "tokenid2".parse().unwrap()], + keep_alive: true, + memo: Some(CborMemo::Raw(Memo::try_from(vec![0, 1]).unwrap())), + }), + metadata: None, + }; + + block_state + .create_lock(&context, configuration1.clone()) + .unwrap(); + let mut lock1 = block_state + .lock_by_id(&context, &lock_id1) + .unwrap() + .expect("lock should exist"); + lock1.add_lock_balance_ref(AccountIndex::from(0), TokenIndex(0)); + lock1.add_lock_balance_ref(AccountIndex::from(1), TokenIndex(1)); + block_state.update_lock(&context, lock1).unwrap(); + let lock_id2 = LockId { + account_index: 2, + sequence_number: 7, + creation_order: 0, + }; + let configuration2 = LockConfiguration { + lock_id: lock_id2.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + block_state + .create_lock(&context, configuration2.clone()) + .unwrap(); + + // Create a third lock and then delete it + let lock_id3 = LockId { + account_index: 3, + sequence_number: 1, + creation_order: 0, + }; + let configuration3 = LockConfiguration { + lock_id: lock_id3.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + block_state.create_lock(&context, configuration3).unwrap(); + let was_deleted = block_state.delete_lock(&context, &lock_id3).unwrap(); + assert!(was_deleted, "lock3 should be deleted"); + + // Store and load block state + let blob_ref = blob_store::store_to_store(&mut context.store, block_state.persistent); + let block_state = entity_test_stub::load_block_state_p11(&context, blob_ref); + + // Assert loaded state + assert_eq!(block_state.lock_list(&context).unwrap().len(), 2); + + // Assert the deleted lock is absent + block_state + .lock_by_id(&context, &lock_id3) + .unwrap() + .expect_err("lock3 should not exist after deletion and reload"); + let lock1 = block_state + .lock_by_id(&context, &lock_id1) + .unwrap() + .unwrap(); + assert_eq!( + lock1.lock_balance_refs(), + vec![ + (AccountIndex::from(0), TokenIndex(0)), + (AccountIndex::from(1), TokenIndex(1)) + ] + ); + assert_eq!( + lock1.lock_configuration(&context).unwrap().into_owned(), + configuration1 + ); + let lock2 = block_state + .lock_by_id(&context, &lock_id2) + .unwrap() + .unwrap(); + assert_eq!(lock2.lock_balance_refs(), vec![]); + assert_eq!( + lock2.lock_configuration(&context).unwrap().into_owned(), + configuration2 + ); + } + + /// Assert that hash and stored bytes of an empty block state matches snapshot. + /// The hash and bytes should remain fixed. + #[test] + fn snapshot_test_hash_and_storage_empty() { + let mut context = entity_test_stub::new_no_external_context(); + let persistent_block_state = PersistentBlockStateP11::default(); + + // Assert hash + let hash = persistent_block_state.hash(&context.store).expect("hash"); + assert_eq!( + format!("{}", hash), + "db35d91962f8f0315adb99d687d65c796ac67f2956b02e80fb667589f64efcb5" + ); + + // Assert storage + blob_store::store_to_store(&mut context.store, &persistent_block_state); + assert_eq!( + hex::encode(context.store.0), + "0000000000000008000000000000000000000000000000080000000000000000000000000000001000000000000000000000000000000010" + ); + } + + /// Assert that hash and stored bytes of a block state with simple + /// tokens and locks matches snapshot. + /// The hash and bytes should remain fixed. + #[test] + fn snapshot_test_hash_and_storage_simple_tokens_and_locks() { + let mut context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index1 = block_state + .create_token(&context, configuration1.clone()) + .unwrap(); + let mut token1 = block_state.token_by_index(&context, token_index1).unwrap(); + token1 + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(100)); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 1], vec![0, 0]) + .unwrap(); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 2], vec![1, 1]) + .unwrap(); + block_state.update_token(&context, token1).unwrap(); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + let _token2 = block_state.create_token(&context, configuration2.clone()); + + // Create locks + let lock_id1 = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration1 = LockConfiguration { + lock_id: lock_id1.clone(), + recipients: LockRecipients::from(vec![AccountIndex::from(1), AccountIndex::from(2)]), + expiry: TransactionTime::from(100u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1), + roles: vec![ + LockControllerSimpleV0Capability::Cancel, + LockControllerSimpleV0Capability::Fund, + ], + }], + tokens: vec!["tokenid1".parse().unwrap(), "tokenid2".parse().unwrap()], + keep_alive: true, + memo: Some(CborMemo::Raw(Memo::try_from(vec![0, 1]).unwrap())), + }), + metadata: None, + }; + block_state.create_lock(&context, configuration1).unwrap(); + let mut lock1 = block_state + .lock_by_id(&context, &lock_id1) + .unwrap() + .expect("lock should exist"); + lock1.add_lock_balance_ref(AccountIndex::from(0), TokenIndex(0)); + lock1.add_lock_balance_ref(AccountIndex::from(1), TokenIndex(1)); + block_state.update_lock(&context, lock1).unwrap(); + let lock_id2 = LockId { + account_index: 2, + sequence_number: 7, + creation_order: 0, + }; + let configuration2 = LockConfiguration { + lock_id: lock_id2.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + block_state.create_lock(&context, configuration2).unwrap(); + + // Assert hash + let hash = block_state.persistent.hash(&context.store).expect("hash"); + assert_eq!( + format!("{}", hash), + "fc27fe3af8be452105e7712ed2139fa637aff5738d678cdbf8f3093ee0506a0b" + ); + + // Assert storage + blob_store::store_to_store(&mut context.store, &block_state.persistent); + assert_eq!( + hex::encode(context.store.0), + "000000000000002806746f6b656e310505050505050505050505050505050505050505050505050505050505050505020000000000000025edbda48b85971b3a874334ca94f07e55e6a6e63eabca968d1257a3223e1b84e14002010100000000000000002503b0eab929105fd6df1ec793cbaf1b554a7a385520a9f7c902adf0219ace6dab4002000000000000000000003648b07111a93452374c7bcf66ee01959af6b4a52cb7cd299341e9ea77b378b0230300000201000000000000005d020000000000000030000000000000000901000000000000008a0000000000000011000000000000000000000000000000c86400000000000000090000000000000000d9000000000000002806746f6b656e3205050505050505050505050505050505050505050505050505050505050505050400000000000000010000000000000000110000000000000103000000000000013300000000000000000900000000000000013c0000000000000021000000000000000201000000000000000000000000000000f20000000000000155000000000000005d0000000000000001000000000000000100000000000000000100020000000000000001000000000000000200000000000000640000010000000000000001020300000208746f6b656e69643108746f6b656e696432010100000200010000000000000000310100000000000000020000000000000000000000000000000000000000000000010000000000000001000000000000018f00000000000000090000000000000001f4000000000000002b000000000000000200000000000000070000000000000000010000000000000000000000000000000000000000000000000011010000000000000000000000000000023e000000000000000900000000000000027100000000000000210000000000000002010000000000000000000000000000022d000000000000028a00000000000000100000000000000166000000000000029b" + ); + } +} diff --git a/plt/plt-block-state/src/persistent/block_state/p9.rs b/plt/plt-block-state/src/persistent/block_state/p9.rs new file mode 100644 index 0000000000..ebeb18f18a --- /dev/null +++ b/plt/plt-block-state/src/persistent/block_state/p9.rs @@ -0,0 +1,297 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreMovable, BlobStoreStore, Loadable, Storable, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use crate::persistent::protocol_level_tokens::p9::PersistentTokensP9; +use concordium_base::common::Buffer; +use concordium_base::hashes::Hash; +use std::io::Read; + +/// P9 block state. +#[derive(Debug, Clone, Default)] +pub struct PersistentBlockStateP9 { + /// Protocol-level tokens + pub(crate) tokens: PersistentTokensP9, +} + +impl Loadable for PersistentBlockStateP9 { + fn load_from_buffer( + buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let tokens = Loadable::load_from_buffer(buffer, loader)?; + + Ok(Self { tokens }) + } +} + +impl Storable for PersistentBlockStateP9 { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.tokens.store_to_buffer(&mut buffer, storer); + } +} + +impl Cacheable for PersistentBlockStateP9 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.tokens.cache_reference_values(loader)?; + Ok(()) + } +} + +impl Hashable for PersistentBlockStateP9 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + self.tokens.hash(loader) + } +} + +impl BlobStoreMovable for PersistentBlockStateP9 { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_tokens = self.tokens.move_blob_store(from_loader, to_storer)?; + + Ok(PersistentBlockStateP9 { tokens: new_tokens }) + } +} + +#[cfg(test)] +mod test { + use crate::entity::block_state::p9::BlockStateP9; + use crate::entity::entity_test_stub; + use crate::entity::entity_test_stub::StubbedNoExternalEntityContext; + use crate::external::test_stub::UnreachableExternalBlockState; + use crate::persistent::blob_store; + use crate::persistent::blob_store::BlobStoreLocation; + use crate::persistent::blob_store::test_stub::BlobStoreStub; + use crate::persistent::block_state::p9::PersistentBlockStateP9; + use crate::persistent::hash::Hashable; + use crate::persistent::protocol_level_tokens::p9::TokenConfiguration; + use concordium_base::protocol_level_tokens::TokenModuleRef; + use plt_scheduler_types::types::tokens::RawTokenAmount; + + /// Store state with PLTs to blob store and load it again. + #[test] + fn test_store_and_load_tokens() { + let mut context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index1 = block_state + .create_token(&context, configuration1.clone()) + .unwrap(); + let mut token1 = block_state.token_by_index(&context, token_index1).unwrap(); + token1 + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(100)); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 1], vec![0, 0]) + .unwrap(); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 2], vec![1, 1]) + .unwrap(); + block_state.update_token(&context, token1).unwrap(); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + let _token_index2 = block_state.create_token(&context, configuration2.clone()); + + // Store and load block state + let blob_ref = blob_store::store_to_store(&mut context.store, block_state.persistent); + let block_state = entity_test_stub::load_block_state_p9(&context, blob_ref); + + // Assert loaded state + assert_eq!(block_state.plt_list(&context).len(), 2); + let token1 = block_state + .token_by_id(&context, &"token1".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token1.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(100) + ); + assert_eq!( + token1.token_p9_base.token_configuration(&context).unwrap(), + configuration1 + ); + let value = token1 + .token_p9_base + .mutable_key_value_state + .lookup_value(&context.store, &[0, 1]); + assert_eq!(value, Some(vec![0, 0])); + let value = token1 + .token_p9_base + .mutable_key_value_state + .lookup_value(&context.store, &[0, 2]); + assert_eq!(value, Some(vec![1, 1])); + let token2 = block_state + .token_by_id(&context, &"token2".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token2.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + assert_eq!( + token2.token_p9_base.token_configuration(&context).unwrap(), + configuration2 + ); + } + + /// Assert that hash of an empty block state matches a fixed/snapshot hash. The hash + /// must remain stable. + #[test] + fn snapshot_test_hash_empty() { + let context = entity_test_stub::new_no_external_context(); + let persistent_block_state = PersistentBlockStateP9::default(); + + // Assert hash + let hash = persistent_block_state.hash(&context.store).expect("hash"); + assert_eq!( + format!("{}", hash), + "c423f9e91ee218b2b5303485dd87a3093a653ddb9bdb839d30aa1924de1dbf05" + ); + } + + /// Assert that hash of block state with some simple PLTs matches a fixed/snapshot hash. The hash + /// must remain stable. + #[test] + fn snapshot_test_hash_simple_tokens() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index1 = block_state + .create_token(&context, configuration1.clone()) + .unwrap(); + let mut token1 = block_state.token_by_index(&context, token_index1).unwrap(); + token1 + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(100)); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 1], vec![0, 0]) + .unwrap(); + token1 + .token_p9_base + .mutable_key_value_state + .insert_value(&context.store, &[0, 2], vec![1, 1]) + .unwrap(); + block_state.update_token(&context, token1).unwrap(); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + let _token2 = block_state.create_token(&context, configuration2.clone()); + + // Assert hash + let hash = block_state.persistent.hash(&context.store).expect("hash"); + assert_eq!( + format!("{}", hash), + "d202e9153fea3fdd22c594be21d471c07e9619abc0baad3faca5c81f0bb1504b" + ); + } + + /// Load empty block state from storage bytes fixture. + /// The fixture bytes must not change and must be compatible with Haskell PLT state implementation. + #[test] + fn fixture_test_storage_empty() { + let store = BlobStoreStub(hex::decode("00000000000000080000000000000000").unwrap()); + let context = StubbedNoExternalEntityContext { + external: UnreachableExternalBlockState, + store, + }; + + // Load block state + let block_state = entity_test_stub::load_block_state_p9(&context, BlobStoreLocation(0)); + + // Assert loaded state + assert_eq!(block_state.plt_list(&context).len(), 0); + } + + /// Load block state with some simple PLTs from storage bytes fixture. + /// The fixture bytes must not change and must be compatible with Haskell PLT state implementation. + #[test] + fn fixture_test_storage_simple_tokens() { + let store = BlobStoreStub(hex::decode("000000000000002806746f6b656e310505050505050505050505050505050505050505050505050505050505050505020000000000000025edbda48b85971b3a874334ca94f07e55e6a6e63eabca968d1257a3223e1b84e14002010100000000000000002503b0eab929105fd6df1ec793cbaf1b554a7a385520a9f7c902adf0219ace6dab4002000000000000000000003648b07111a93452374c7bcf66ee01959af6b4a52cb7cd299341e9ea77b378b0230300000201000000000000005d020000000000000030000000000000000901000000000000008a0000000000000011000000000000000000000000000000c86400000000000000090000000000000000d9000000000000002806746f6b656e3205050505050505050505050505050505050505050505050505050505050505050400000000000000010000000000000000110000000000000103000000000000013300000000000000000900000000000000013c0000000000000021000000000000000201000000000000000000000000000000f20000000000000155").unwrap()); + + let context = StubbedNoExternalEntityContext { + external: UnreachableExternalBlockState, + store, + }; + + // Load block state + let block_state = entity_test_stub::load_block_state_p9(&context, BlobStoreLocation(358)); + + // Assert loaded state + assert_eq!(block_state.plt_list(&context).len(), 2); + let token1 = block_state + .token_by_id(&context, &"token1".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token1.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(100) + ); + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + assert_eq!( + token1.token_p9_base.token_configuration(&context).unwrap(), + configuration1 + ); + let value = token1 + .token_p9_base + .mutable_key_value_state + .lookup_value(&context.store, &[0, 1]); + assert_eq!(value, Some(vec![0, 0])); + let value = token1 + .token_p9_base + .mutable_key_value_state + .lookup_value(&context.store, &[0, 2]); + assert_eq!(value, Some(vec![1, 1])); + let token2 = block_state + .token_by_id(&context, &"token2".parse().unwrap()) + .unwrap() + .unwrap(); + assert_eq!( + token2.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + assert_eq!( + token2.token_p9_base.token_configuration(&context).unwrap(), + configuration2 + ); + } +} diff --git a/plt/plt-block-state/src/persistent/cacheable.rs b/plt/plt-block-state/src/persistent/cacheable.rs new file mode 100644 index 0000000000..1c238eba5d --- /dev/null +++ b/plt/plt-block-state/src/persistent/cacheable.rs @@ -0,0 +1,33 @@ +//! Definition of the [`Cacheable`] trait that allows caching block state components wrapped in +//! [blob references](super::blob_reference) into memory. + +use crate::failure::BlockStateResult; +use crate::persistent::blob_store::{BlobStoreLoad, StoreSerialized}; +use concordium_base::common::Deserial; + +/// Trait implemented by types that are stored in the blob store and may +/// be composed of [blob references](super::blob_reference) +/// that represents values that can be cached into memory on demand. +pub trait Cacheable { + /// Load any values pointed to by [blob references](super::blob_reference) + /// into memory in a cached representation. + /// This operation should recursively apply the cache operation as values are cached into memory, + /// and values that are already in memory. + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()>; +} + +impl Cacheable for StoreSerialized { + fn cache_reference_values(&self, _loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + // nothing to cache, if a value is directly deserializable + Ok(()) + } +} + +impl Cacheable for Option { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + match self { + Some(inner) => inner.cache_reference_values(loader), + None => Ok(()), + } + } +} diff --git a/plt/plt-block-state/src/persistent/chain_parameters.rs b/plt/plt-block-state/src/persistent/chain_parameters.rs new file mode 100644 index 0000000000..9a0dac015b --- /dev/null +++ b/plt/plt-block-state/src/persistent/chain_parameters.rs @@ -0,0 +1,109 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreLocation, BlobStoreStore, Loadable, Storable, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::chain_parameters::p11::PersistentChainParametersP11; +use crate::persistent::hash::Hashable; +use concordium_base::common::Buffer; +use concordium_base::contracts_common::Duration; +use concordium_base::hashes::Hash; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; +use std::any; +use std::io::Read; + +pub mod p11; + +/// Persistent node-owned chain parameters managed by Rust. +#[derive(Debug, Clone)] +pub enum PersistentChainParameters { + /// P11 external chain parameters. + P11(PersistentChainParametersP11), +} + +impl PersistentChainParameters { + /// Construct P11 persistent chain parameters with an initial maximum lock duration. + pub fn p11_new_external_chain_parameters(max_lock_duration: Duration) -> Self { + Self::P11(PersistentChainParametersP11 { max_lock_duration }) + } + + /// Load persistent chain parameters from the blob store. + pub fn load_from_store( + loader: &impl BlobStoreLoad, + location: BlobStoreLocation, + protocol_version: ProtocolVersion, + ) -> BlockStateResult { + let bytes = loader.load_raw(location); + let mut bytes_slice = bytes.as_slice(); + let value = Self::load_from_buffer(&mut bytes_slice, loader, protocol_version)?; + if !bytes_slice.is_empty() { + return Err(BlockStateFailure::BlobStoreDecode(format!( + "Bytes remaining after loading value of type {} from blob store", + any::type_name::() + ))); + }; + Ok(value) + } + + /// Load persistent chain parameters from bytes for the given protocol version. + fn load_from_buffer( + buffer: impl Read, + loader: &impl BlobStoreLoad, + protocol_version: ProtocolVersion, + ) -> BlockStateResult { + match protocol_version { + ProtocolVersion::P11 => Ok(Self::P11(Loadable::load_from_buffer(buffer, loader)?)), + ProtocolVersion::P9 | ProtocolVersion::P10 => { + panic!("No Rust-managed external chain parameters before P11") + } + } + } +} + +impl Storable for PersistentChainParameters { + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + match self { + PersistentChainParameters::P11(params) => params.store_to_buffer(buffer, storer), + } + } +} + +impl Cacheable for PersistentChainParameters { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + match self { + PersistentChainParameters::P11(params) => params.cache_reference_values(loader), + } + } +} + +impl Hashable for PersistentChainParameters { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + match self { + PersistentChainParameters::P11(params) => params.hash(loader), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::persistent::blob_store; + use crate::persistent::blob_store::test_stub::BlobStoreStub; + + #[test] + fn store_load_roundtrip() { + let mut store = BlobStoreStub::default(); + let params = + PersistentChainParameters::p11_new_external_chain_parameters(Duration::from_millis(42)); + let location = blob_store::store_to_store(&mut store, ¶ms); + let loaded = + PersistentChainParameters::load_from_store(&store, location, ProtocolVersion::P11) + .expect("external chain parameters should load"); + + match loaded { + PersistentChainParameters::P11(params) => { + assert_eq!(params.max_lock_duration, Duration::from_millis(42)) + } + } + } +} diff --git a/plt/plt-block-state/src/persistent/chain_parameters/p11.rs b/plt/plt-block-state/src/persistent/chain_parameters/p11.rs new file mode 100644 index 0000000000..e321093dad --- /dev/null +++ b/plt/plt-block-state/src/persistent/chain_parameters/p11.rs @@ -0,0 +1,85 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{BlobStoreLoad, BlobStoreStore, Loadable, Storable}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash; +use crate::persistent::hash::Hashable; +use concordium_base::common::{Buffer, Get, Put}; +use concordium_base::contracts_common::Duration; +use concordium_base::hashes::Hash; +use std::io::Read; + +/// Node-owned persistent external chain parameters for P11. +/// +/// This mirrors the parts of the public chain-parameter view whose authoritative +/// state is managed outside the ordinary Haskell chain-parameter record. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PersistentChainParametersP11 { + /// Maximum relative duration for protocol-level token locks, in milliseconds. + pub max_lock_duration: Duration, +} + +impl Loadable for PersistentChainParametersP11 { + fn load_from_buffer( + mut buffer: impl Read, + _loader: &impl BlobStoreLoad, + ) -> Result { + let max_lock_duration = buffer.get().map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Error parsing P11 chain-parameters max_lock_duration: {err}" + )) + })?; + Ok(Self { max_lock_duration }) + } +} + +impl Storable for PersistentChainParametersP11 { + fn store_to_buffer(&self, mut buffer: impl Buffer, _storer: &mut impl BlobStoreStore) { + buffer.put(self.max_lock_duration); + } +} + +impl Cacheable for PersistentChainParametersP11 { + fn cache_reference_values(&self, _loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + Ok(()) + } +} + +impl Hashable for PersistentChainParametersP11 { + fn hash(&self, _loader: &impl BlobStoreLoad) -> BlockStateResult { + Ok(hash::hash_of_serialization(self.max_lock_duration)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::persistent::blob_store; + use crate::persistent::blob_store::test_stub::BlobStoreStub; + + #[test] + fn store_load_roundtrip() { + let mut store = BlobStoreStub::default(); + let params = PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(42), + }; + let location = blob_store::store_to_store(&mut store, ¶ms); + let loaded: PersistentChainParametersP11 = blob_store::load_from_store(&store, location) + .expect("P11 chain parameters should load"); + assert_eq!(params, loaded); + } + + #[test] + fn hash_changes_with_max_lock_duration() { + let store = BlobStoreStub::default(); + let zero = PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(0), + }; + let non_zero = PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(42), + }; + assert_ne!( + zero.hash(&store).expect("zero hash"), + non_zero.hash(&store).expect("non-zero hash") + ); + } +} diff --git a/plt/plt-block-state/src/persistent/hash.rs b/plt/plt-block-state/src/persistent/hash.rs new file mode 100644 index 0000000000..23ae9e83bc --- /dev/null +++ b/plt/plt-block-state/src/persistent/hash.rs @@ -0,0 +1,50 @@ +//! Definition of the [`Hashable`] trait that allows hashing block state components. + +use crate::failure::BlockStateResult; +use crate::persistent::blob_store::{BlobStoreLoad, StoreSerialized}; +use concordium_base::common::{Put, Serial}; +use concordium_base::hashes::Hash; +use sha2::Digest; + +/// Trait implemented by hashable values, that potentially needs +/// to load values from the blob store to calculate the hash. +pub trait Hashable { + /// Calculate hash of value. The given blob store `loader` can be used + /// to load values pointed to by [blob references](super::blob_reference) + /// from the blob store. + /// The loaded values should generally not be cached as a side effect. But the + /// hash calculated from loaded values should generally be cached and reused for the + /// next hash calculation, such that loading from the blob store is not necessary + /// if the hash needs to be calculated again. + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult; +} + +impl Hashable for StoreSerialized { + fn hash(&self, _loader: &impl BlobStoreLoad) -> BlockStateResult { + Ok(hash_of_serialization(&self.0)) + } +} + +impl Hashable for Option { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + match self { + None => Ok(hash_of_serialization(0u8)), + Some(inner) => Ok(hash_of_serialization((1u8, inner.hash(loader)?))), + } + } +} + +/// Calculate hash by digesting the bytes of two hashes. +pub fn hash_of_hashes(hash1: Hash, hash2: Hash) -> Hash { + let mut hasher = sha2::Sha256::new(); + hasher.update(hash1); + hasher.update(hash2); + Hash::new(hasher.finalize().into()) +} + +/// Calculate hash by digesting the serialized bytes of a value. +pub fn hash_of_serialization(value: impl Serial) -> Hash { + let mut hasher = sha2::Sha256::new(); + hasher.put(value); + Hash::new(hasher.finalize().into()) +} diff --git a/plt/plt-block-state/src/persistent/lfmb_tree.rs b/plt/plt-block-state/src/persistent/lfmb_tree.rs new file mode 100644 index 0000000000..9c23d70fd1 --- /dev/null +++ b/plt/plt-block-state/src/persistent/lfmb_tree.rs @@ -0,0 +1,1467 @@ +//! Representation of an immutable, left-full merkle binary (LFMB) tree. +//! +//! See [`LfmbTree`]. + +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreMovable, BlobStoreStore, Loadable, ParseResultExt, Storable, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash; +use crate::persistent::hash::Hashable; +use crate::utils::Cow; +use concordium_base::common::{Buffer, Get, Put}; +use concordium_base::hashes::Hash; +use either::Either; +use sha2::{Digest, Sha256}; +use std::fmt::Debug; +use std::io::Read; +use std::marker::PhantomData; +use std::{iter, vec}; + +/// Representation of an immutable, left-full merkle binary (LFMB) tree with values of type `V`. +/// The represented tree is immutable in the sense that the tree and its values does not change, +/// once it has been created. When values are inserted or updated, a new tree is created, +/// reusing the nodes that have not changed by the operation. +/// Keys are assigned as a sequential list of integers starting from 0. Hence, the tree always fills up left branches +/// first. +/// +/// The operations supported for creating new trees are: +/// * Create empty tree with [`LfmbTree::empty`]: Returns a new empty tree. +/// * Insert new value with [`LfmbTree::insert_value`]: Inserts a new value, assigning the sequentially next unused key, +/// and returns new tree with the inserted value. +/// * Update value with [`LfmbTree::update_value`]: Updates an existing value, keeping the same key, and returns +/// the new tree with the updated value. +/// +/// ## Interior mutability +/// +/// The internal representation in the tree may change during the lifetime via interior mutability. +/// This happens if values are cached, stored or hashes are lazily calculated. +/// +/// ## Data structure +/// +/// The data structure is a left-full binary tree where keys are assigned as a sequence of integers +/// starting with 0. There are no gaps in the sequence of keys, since entries are never removed. +/// That keys are assigned sequentially from 0 means that the tree can be maintained as left-full. +/// Each [node](Subtree::Node) has a height `h`, and the following invariant holds for the size of the two subtrees: +/// +/// * *size of left subtree* `== 2^h` +/// * `0 <` *size of right subtree* `<= 2^h` +/// +/// Notice that this gives a size invariant for node: `2^h <` *size of node* `<= 2^(h + 1)`. +/// +/// This invariant for the nodes subtree sizes uniquely determines the structure of a tree of +/// a given size. See the examples below. +/// As the tree grows, new nodes are inserted in the tree on top off full subtrees or leafs. +/// +/// Given the node subtree size invariant above, we can locate the leaf for a key by traversing the tree starting +/// at the root, and for each [node](Subtree::Node), use the height `h` to decide +/// on which branch to take based on the `h`'th bit in the key: +/// +/// * `0`: follow left branch, +/// * `1`: follow right branch, +/// +/// The algorithms for specific operations are described in more detail in the implementations: +/// +/// * [`Subtree::insert_value`] +/// * [`Subtree::lookup_value`] +/// +/// ### Enforcing invariants +/// +/// Loading a tree from the blob store may result in broken invariants, +/// if the blob store is corrupted in some way. The implemented operations will return +/// [`BlockStateFailure::Invariant`] if broken invariants are encountered. +/// +/// ### Example tree +/// +/// This section contains diagrams of trees up to size eight. The tree are a result of +/// inserting letters as values alphabetically `A`, `B`, `C`, ... . +/// +/// The symbols used in the diagrams are +/// +/// * `[A]`: Leaf with value `A`. +/// * `(h)`: Node at height `h`. +/// * `#i`: Key label indicating that leaf corresponds to key `i`. +/// Notice that this is not part of the data structure, it is implicitly derived. +/// +/// ```text +/// +/// size 1: +/// [A] +/// #0 +/// +/// size 2: +/// (0) +/// / \ +/// [A] [B] +/// #0 #1 +/// +/// size 3: +/// (1) +/// / \ +/// (0) [C] +/// / \ #2 +/// [A] [B] +/// #0 #1 +/// +/// size 4: +/// (1) +/// / \ +/// (0) (0) +/// / \ / \ +/// [A] [B] [C] [D] +/// #0 #1 #2 #3 +/// +/// size 5: +/// (2) +/// / \ +/// (1) [E] +/// / \ #4 +/// (0) (0) +/// / \ / \ +/// [A] [B] [C] [D] +/// #0 #1 #2 #3 +/// +/// size 6: +/// ---(2)--- +/// / \ +/// (1) (0) +/// / \ / \ +/// (0) (0) [E] [F] +/// / \ / \ #4 #5 +/// [A] [B] [C] [D] +/// #0 #1 #2 #3 +/// +/// size 7: +/// _----(2)----_ +/// / \ +/// (1) (1) +/// / \ / \ +/// (0) (0) (0) [G] +/// / \ / \ / \ #6 +/// [A] [B] [C] [D] [E] [F] +/// #0 #1 #2 #3 #4 #5 +/// +/// size 8: +/// _----(2)----_ +/// / \ +/// (1) (1) +/// / \ / \ +/// (0) (0) (0) (0) +/// / \ / \ / \ / \ +/// [A] [B] [C] [D] [E] [F] [G] [H] +/// #0 #1 #2 #3 #4 #5 #6 #7 +/// ``` +#[derive(Debug)] +pub struct LfmbTree { + inner: LfmbTreeInner, + _key_type: PhantomData, +} + +impl Clone for LfmbTree { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _key_type: self._key_type, + } + } +} + +impl Default for LfmbTree { + fn default() -> Self { + Self::empty() + } +} + +/// Trait implemented by tree keys, which allows them to be bijectively mapped +/// to `u64` values. +pub trait LfmbTreeKey: Copy { + /// Map key to `u64` + fn to_u64(self) -> u64; + + /// Map `u64` to key + fn from_u64(key: u64) -> Self; +} + +impl LfmbTree { + /// Create an empty tree. + pub fn empty() -> Self { + let inner = LfmbTreeInner::Empty; + + Self { + inner, + _key_type: PhantomData, + } + } + + /// Return the number of entries in the tree. + #[allow(unused)] + pub fn size(&self) -> u64 { + match &self.inner { + LfmbTreeInner::Empty => 0, + LfmbTreeInner::NonEmpty(size, _) => *size, + } + } + + /// Get the value for the given `key` in the tree or `None` if there + /// is no value for the key. + /// + /// # Arguments + /// + /// - `loader`: Loader for the blob store the tree is stored in. + /// - `key`: The key to access the value for. + /// + /// # Errors + /// + /// Returns [`BlockStateFailure`] if decoding data from the + /// blob store fails, or if the tree does not fulfill + /// the expected invariants (this can happen if the blob store is corrupted in some way). + pub fn lookup_value( + &self, + loader: &impl BlobStoreLoad, + key: K, + ) -> BlockStateResult>> + where + V: Loadable, + { + Ok(match &self.inner { + LfmbTreeInner::Empty => None, + LfmbTreeInner::NonEmpty(size, subtree) => { + let int_key = SubtreeKey(key.to_u64()); + if int_key.0 < *size { + Some(Cow::Borrowed(subtree).lookup_value(loader, int_key)?) + } else { + None + } + } + }) + } + + /// Iterates all values in the tree in insertion order (which is also the order + /// of the keys). + /// + /// # Arguments + /// + /// - `loader`: Loader for the blob store the tree is stored in. + /// + /// # Errors + /// + /// Returns [`BlockStateFailure`] if decoding data from the blob store fails, or if the tree + /// does not fulfill the expected invariants (this can happen if the blob store is + /// corrupted in some way). + pub fn values( + &self, + loader: &impl BlobStoreLoad, + ) -> impl ExactSizeIterator)>> + where + V: Loadable, + { + match &self.inner { + LfmbTreeInner::Empty => Either::Left(iter::empty()), + LfmbTreeInner::NonEmpty(_, subtree) => { + Either::Right(subtree.values(loader, self.size()).map(|item| { + let (subtree_key, value) = item?; + Ok((K::from_u64(subtree_key.0), value)) + })) + } + } + } + + /// Insert a value to the tree, and return the key for the inserted value and + /// the tree with the inserted value. Keys are assigned sequentially, + /// starting from `LfmbTreeKey::from_u64(0)`, then `LfmbTreeKey::from_u64(1)` and + /// so on. + /// + /// Notice that trees are immutable data structures, see [`Self`]. + /// + /// # Arguments + /// + /// - `loader`: loader for the blob store the tree is stored in + /// - `value`: The value to insert + /// + /// # Errors + /// + /// Returns [`BlockStateFailure`] if decoding data from the + /// blob store fails, or if the tree does not fulfill + /// the expected invariants (this can happen if the blob store is corrupted in some way). + pub fn insert_value(&self, loader: &impl BlobStoreLoad, value: V) -> BlockStateResult<(K, Self)> + where + V: Loadable, + { + Ok(match &self.inner { + LfmbTreeInner::Empty => ( + LfmbTreeKey::from_u64(0), + Self::from_inner(LfmbTreeInner::NonEmpty( + 1, + Subtree::Leaf(HashedCacheableRef::new(value)), + )), + ), + LfmbTreeInner::NonEmpty(size, subtree) => ( + LfmbTreeKey::from_u64(*size), + Self::from_inner(LfmbTreeInner::NonEmpty( + *size + 1, + subtree.insert_value(loader, None, *size, value)?, + )), + ), + }) + } + + /// Update the value with the given `key` in the tree + /// using the `update` closure. Returns the tree with the updated + /// value or `None` if there is no entry with the given key in the tree. + /// + /// Notice that trees are immutable data structures, see [`Self`]. + /// + /// # Arguments + /// + /// - `loader`: Loader for the blob store the tree is stored in. + /// - `key`: The key to update the value for. + /// - `update`: Closure that is given the value, either as owned + /// or borrowed, and returns the new value for the key. + /// + /// # Errors + /// + /// Returns [`BlockStateFailure`] if returned by `update` or if decoding data from the + /// blob store fails, or if the tree does not fulfill + /// the expected invariants (this can happen if the blob store is corrupted in some way). + pub fn update_value( + &self, + loader: &impl BlobStoreLoad, + key: K, + update: impl FnOnce(Cow<'_, V>) -> BlockStateResult, + ) -> BlockStateResult> + where + V: Loadable, + { + Ok(match &self.inner { + LfmbTreeInner::Empty => None, + LfmbTreeInner::NonEmpty(size, subtree) => { + let int_key = SubtreeKey(key.to_u64()); + if int_key.0 < *size { + let new_subtree = subtree.update_value(loader, int_key, update)?; + Some(Self::from_inner(LfmbTreeInner::NonEmpty( + *size, + new_subtree, + ))) + } else { + None + } + } + }) + } + + fn from_inner(inner: LfmbTreeInner) -> Self { + Self { + inner, + _key_type: Default::default(), + } + } +} + +/// Internal representation of tree key used in the subtree. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +struct SubtreeKey(u64); + +/// Internal representation of the tree. +#[derive(Debug)] +enum LfmbTreeInner { + /// Empty Tree. + Empty, + /// Non-empty tree. + /// + /// Invariant: The number of entries in the subtree + /// is equal to size. + NonEmpty( + /// Size + u64, + /// Root + Subtree, + ), +} + +impl Clone for LfmbTreeInner { + fn clone(&self) -> Self { + match self { + LfmbTreeInner::Empty => LfmbTreeInner::Empty, + LfmbTreeInner::NonEmpty(size, subtree) => { + LfmbTreeInner::NonEmpty(*size, subtree.clone()) + } + } + } +} + +/// Non-empty subtree. The type is used recursively to represent branches. +#[derive(Debug)] +enum Subtree { + /// Leaf with value + Leaf(HashedCacheableRef), + /// Node with two subtrees/branches. + /// + /// Invariant relating height `h` and branches: + /// * *size of left subtree* `== 2^h` + /// * `0 <` *size of right subtree* `<= 2^h` + Node( + /// Height of tree + u64, + /// Left branch + HashedCacheableRef>, + /// Right branch + HashedCacheableRef>, + ), +} + +impl Clone for Subtree { + fn clone(&self) -> Self { + match self { + Subtree::Leaf(val_ref) => Subtree::Leaf(val_ref.clone()), + Subtree::Node(height, left_ref, right_ref) => { + Subtree::Node(*height, left_ref.clone(), right_ref.clone()) + } + } + } +} + +/// [`Subtree`] with [`Cow`] structurally projected. +/// Used as return value for [`Cow::cow_project_structural`]. +#[derive(Debug)] +enum SubtreeCowProjection<'b, V> { + /// Leaf with value. See [`Subtree::Leaf`] + Leaf(Cow<'b, HashedCacheableRef>), + /// Node with two subtrees/branches. + /// See [`Subtree::Node`] + Node( + /// Height of tree + u64, + /// Left branch + Cow<'b, HashedCacheableRef>>, + /// Right branch + Cow<'b, HashedCacheableRef>>, + ), +} + +/// Check if `nth` bit is set in `key`. +const fn is_nth_bit_set(nth: u64, key: SubtreeKey) -> bool { + let bit = 1u64 << nth; + (key.0 & bit) != 0 +} + +/// Flip the `nth` bit in `key`. +const fn flip_nth_bit(nth: u64, key: SubtreeKey) -> SubtreeKey { + let bit = 1u64 << nth; + SubtreeKey(key.0 ^ bit) +} + +impl<'b, V> Cow<'b, Subtree> { + /// Get the value for the given `key` in the subtree. + /// + /// # Arguments + /// + /// - `key`: The key to access the value for. + /// + /// # Panic + /// + /// If the given subtree is owned, it is a precondition that the leaf reference + /// and branch subtree references does not return borrowed values when calling + /// [`HashedCacheableRef::value`] on them. + /// Else the present function will panic. If the given subtree is borrowed, + /// there is no precondition for using the function. + /// See [`Cow::bind_value`] for further details. + /// + /// Notice that for an owned or borrowed subtree that fulfills the precondition, loading + /// the left and right branches with [`HashedCacheableRef::value`] will give + /// owned or borrowed subtrees, that recursively fulfills the precondition. + fn lookup_value( + self, + loader: &impl BlobStoreLoad, + key: SubtreeKey, + ) -> BlockStateResult> + where + V: Loadable, + { + Ok(match self.cow_project_structural() { + SubtreeCowProjection::Leaf(val_ref) => { + // When we reach the leaf for the key, the key must be 0. + if key != SubtreeKey(0) { + return Err(BlockStateFailure::Invariant(format!( + "LFMB Subtree invariant broken: SubtreeKey must be zero at leaf, is {:?}", + key + ))); + } + val_ref.bind_value(loader, "leaf in lfmb_tree::Subtree")? + } + SubtreeCowProjection::Node(height, left_ref, right_ref) => { + // The height'th bit in key decides if we should follow left `0` + // or right branch `1`. Additionally, when going right, we set the bit to 0. + // This allows us to check the invariant that the key must be identical to 0 + // when we reach the leaf for the key. + if is_nth_bit_set(height, key) { + right_ref + .bind_value(loader, "left branch in lfmb_tree::Subtree")? + .lookup_value(loader, flip_nth_bit(height, key))? + } else { + left_ref + .bind_value(loader, "right branch in lfmb_tree::Subtree")? + .lookup_value(loader, key)? + } + } + }) + } + + /// Move [`Cow`] inside the subtree wrapping blob references directly. + fn cow_project_structural(self) -> SubtreeCowProjection<'b, V> + where + V: Loadable, + { + match self { + Cow::Borrowed(Subtree::Leaf(value_ref)) => { + SubtreeCowProjection::Leaf(Cow::Borrowed(value_ref)) + } + Cow::Borrowed(Subtree::Node(height, left_ref, right_ref)) => { + SubtreeCowProjection::Node( + *height, + Cow::Borrowed(left_ref), + Cow::Borrowed(right_ref), + ) + } + Cow::Owned(Subtree::Leaf(value_ref)) => { + SubtreeCowProjection::Leaf(Cow::Owned(value_ref)) + } + Cow::Owned(Subtree::Node(height, left_ref, right_ref)) => { + SubtreeCowProjection::Node(height, Cow::Owned(left_ref), Cow::Owned(right_ref)) + } + } + } +} + +impl Subtree { + /// Iterates all values in the subtree in insertion order. + /// + /// # Arguments + /// + /// - `node_size`: The number of entries in the subtree. + pub fn values( + &self, + loader: &impl BlobStoreLoad, + node_size: u64, + ) -> impl ExactSizeIterator)>> + where + V: Loadable, + { + ValuesIterator::new(self, loader, node_size) + } + + /// Insert `new_value` into the subtree and return a new subtree with the inserted value. + /// + /// # Arguments + /// + /// - `subtree_ref_option`: Blob reference to the subtree. For the root tree, there is no such + /// reference, in which case the argument is `None`. + /// - `node_size`: The number of entries in the subtree. + /// - `new_value`: The value to insert in the subtree. + fn insert_value( + &self, + loader: &impl BlobStoreLoad, + subtree_ref_option: Option<&HashedCacheableRef>>, + node_size: u64, + new_value: V, + ) -> BlockStateResult + where + V: Loadable, + { + Ok(match self { + Subtree::Leaf(current_val_ref) => { + if node_size != 1 { + return Err(BlockStateFailure::Invariant(format!( + "LFMB Subtree invariant broken: Expected size 1 for leaf, is {:?}", + node_size + ))); + } + // Create node with height 0 with current value as the left side, and new value as the right side. + Subtree::Node( + 0, + HashedCacheableRef::new(Subtree::Leaf(current_val_ref.clone())), + HashedCacheableRef::new(Subtree::Leaf(HashedCacheableRef::new(new_value))), + ) + } + Subtree::Node(height, left_ref, right_ref) => { + // Left branch is always filled with 2^height entries. + let left_branch_size = 1u64 << height; + // The max node size is the double of that. + let node_max_size = left_branch_size << 1; + + if node_size > node_max_size || node_size <= left_branch_size { + return Err(BlockStateFailure::Invariant(format!( + "LFMB Subtree invariant broken: Node size {} for node with height {}", + node_size, height + ))); + } + // Check if subtree is already full + if node_size == node_max_size { + let subtree_ref = match subtree_ref_option { + None => HashedCacheableRef::new(Subtree::Node( + *height, + left_ref.clone(), + right_ref.clone(), + )), + Some(subtree_ref) => subtree_ref.clone(), + }; + // There is no more room in the subtree, so we insert a new node + // with current node as left side, and the new value as the right side. + // The height of the new node is one higher than the existing. + Subtree::Node( + *height + 1, + subtree_ref, + HashedCacheableRef::new(Subtree::Leaf(HashedCacheableRef::new(new_value))), + ) + } else { + // There is still room in the right branch of the tree, so insert the new value in the right branch. + let new_right = right_ref.value(loader)?.insert_value( + loader, + Some(right_ref), + node_size - left_branch_size, + new_value, + )?; + Subtree::Node( + *height, + left_ref.clone(), + HashedCacheableRef::new(new_right), + ) + } + } + }) + } + + /// Update the value with the given `key` in the tree + /// using the `update` closure. + /// + /// # Arguments + /// + /// - `key`: The key to update the value for. + /// - `update`: Closure that is given the value, either as owned + /// or borrowed, and returns the new value for the key. + pub fn update_value( + &self, + loader: &impl BlobStoreLoad, + key: SubtreeKey, + update: impl FnOnce(Cow<'_, V>) -> BlockStateResult, + ) -> BlockStateResult + where + V: Loadable, + { + Ok(match self { + Subtree::Leaf(val_ref) => { + // When we reach the leaf for the key, the key must be 0. + if key != SubtreeKey(0) { + return Err(BlockStateFailure::Invariant(format!( + "LFMB Subtree invariant broken: SubtreeKey must be zero at leaf, is {:?}", + key + ))); + } + let new_value = update(val_ref.value(loader)?)?; + Subtree::Leaf(HashedCacheableRef::new(new_value)) + } + Subtree::Node(height, left_ref, right_ref) => { + // The height'th bit in key decides if we should follow left `0` + // or right branch `1`. Additionally, when going right, we set the bit to 0. + // This allows us to check the invariant that the key must be identical to 0 + // when we reach the leaf for the key. + if is_nth_bit_set(*height, key) { + let new_right = right_ref.value(loader)?.update_value( + loader, + flip_nth_bit(*height, key), + update, + )?; + + Subtree::Node( + *height, + left_ref.clone(), + HashedCacheableRef::new(new_right), + ) + } else { + let new_left = left_ref.value(loader)?.update_value(loader, key, update)?; + + Subtree::Node( + *height, + HashedCacheableRef::new(new_left), + right_ref.clone(), + ) + } + } + }) + } +} + +/// Iterator of values in tree. +struct ValuesIterator<'a, 'b, L, V> { + /// Blob store loader reference + loader: &'a L, + /// Stack of next nodes to visit. + /// All nodes in the stack must fulfill the precondition described on + /// [`Cow::lookup_value`]. + node_stack: Vec>>, + /// Size of the full tree (constant). + tree_size: u64, + /// Key of next item to be returned by the iterator. + next_key: SubtreeKey, +} + +impl<'a, 'b, L: BlobStoreLoad, V> ValuesIterator<'a, 'b, L, V> { + fn new(subtree: &'b Subtree, loader: &'a L, tree_size: u64) -> Self { + Self { + loader, + node_stack: vec![Cow::Borrowed(subtree)], + tree_size, + next_key: SubtreeKey(0), + } + } +} + +impl<'a, 'b, L: BlobStoreLoad, V: Loadable> ExactSizeIterator for ValuesIterator<'a, 'b, L, V> {} + +impl<'a, 'b, L: BlobStoreLoad, V: Loadable> Iterator for ValuesIterator<'a, 'b, L, V> { + type Item = BlockStateResult<(SubtreeKey, Cow<'b, V>)>; + + fn next(&mut self) -> Option { + if let Some(next_node_ref) = self.node_stack.pop() { + if self.next_key.0 == self.tree_size { + return Some(Err(BlockStateFailure::Invariant( + "LFMB Subtree invariant broken: ValuesIterator next_key equal to tree_size before end of iterator" + .to_string(), + ))); + } + let key = self.next_key; + self.next_key.0 += 1; + Some( + next_value_push_right_branches(self.loader, next_node_ref, &mut self.node_stack) + .map(|v| (key, v)), + ) + } else { + if self.next_key.0 != self.tree_size { + return Some(Err(BlockStateFailure::Invariant(format!( + "LFMB Subtree invariant broken: ValuesIterator next_key not equal to tree_size at end of iterator, is {}", + self.next_key.0 + )))); + } + None + } + } + + fn size_hint(&self) -> (usize, Option) { + let size = (self.tree_size - self.next_key.0) as usize; + (size, Some(size)) + } +} + +/// Follow left branches to reach next value while pushing all right branches to the +/// node stack. +/// +/// # Panic +/// +/// Panics if the precondition described on [`Cow::lookup_value`] is +/// not fulfilled for the subtree. +fn next_value_push_right_branches<'b, L: BlobStoreLoad, V: Loadable>( + loader: &L, + subtree: Cow<'b, Subtree>, + node_stack: &mut Vec>>, +) -> BlockStateResult> { + Ok(match subtree.cow_project_structural() { + SubtreeCowProjection::Leaf(val_ref) => { + val_ref.bind_value(loader, "leaf in lfmb_tree::Subtree")? + } + SubtreeCowProjection::Node(_, left_ref, right_ref) => { + let right = right_ref.bind_value(loader, "left branch in lfmb_tree::Subtree")?; + node_stack.push(right); + let left = left_ref.bind_value(loader, "right branch in lfmb_tree::Subtree")?; + next_value_push_right_branches(loader, left, node_stack)? + } + }) +} + +impl Loadable for LfmbTree { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + let size: u64 = buffer.get().map_parse_err_to_block_state_err()?; + let inner = if size == 0 { + LfmbTreeInner::Empty + } else { + let tree = Subtree::load_from_buffer(buffer, loader)?; + LfmbTreeInner::NonEmpty(size, tree) + }; + + Ok(Self { + inner, + _key_type: PhantomData, + }) + } +} + +impl Storable for LfmbTree { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + match &self.inner { + LfmbTreeInner::Empty => { + buffer.put(0u64); + } + LfmbTreeInner::NonEmpty(size, tree) => { + buffer.put(size); + tree.store_to_buffer(buffer, storer); + } + } + } +} + +impl Loadable for Subtree { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + // Notice that the load implementation must ensure that the precondition + // on with_referenced_values is fulfilled for newly loaded subtrees. + let disc: u8 = buffer.get().map_parse_err_to_block_state_err()?; + Ok(match disc { + 0 => { + let value_ref = Loadable::load_from_buffer(buffer, loader)?; + Subtree::Leaf(value_ref) + } + 1 => { + let height: u64 = buffer.get().map_parse_err_to_block_state_err()?; + let left_ref = Loadable::load_from_buffer(&mut buffer, loader)?; + let right_ref = Loadable::load_from_buffer(&mut buffer, loader)?; + Subtree::Node(height, left_ref, right_ref) + } + _ => { + return Err(BlockStateFailure::BlobStoreDecode(format!( + "Invalid LFMB Tree discriminator: {}", + disc + ))); + } + }) + } +} + +impl Storable for Subtree { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + match self { + Subtree::Leaf(value) => { + buffer.put(0u8); + value.store_to_buffer(buffer, storer); + } + Subtree::Node(height, left_ref, right_ref) => { + buffer.put(1u8); + buffer.put(height); + left_ref.store_to_buffer(&mut buffer, storer); + right_ref.store_to_buffer(&mut buffer, storer); + } + } + } +} + +impl Hashable for LfmbTree { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + let mut hasher = sha2::Sha256::new(); + + match &self.inner { + LfmbTreeInner::Empty => { + hasher.update(0u64.to_be_bytes()); + hasher.update(Sha256::digest("EmptyLFMBTree")); + } + LfmbTreeInner::NonEmpty(size, subtree) => { + hasher.update(size.to_be_bytes()); + hasher.update(subtree.hash(loader)?); + } + } + Ok(Hash::new(hasher.finalize().into())) + } +} + +impl Hashable for Subtree { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + Ok(match self { + Subtree::Node(_, left_ref, right_ref) => { + hash::hash_of_hashes(left_ref.hash(loader)?, right_ref.hash(loader)?) + } + Subtree::Leaf(v) => v.hash(loader)?, + }) + } +} + +impl Cacheable for LfmbTree { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + match &self.inner { + LfmbTreeInner::Empty => (), + LfmbTreeInner::NonEmpty(_, subtree) => { + subtree.cache_reference_values(loader)?; + } + } + Ok(()) + } +} + +impl Cacheable for Subtree { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + match self { + Subtree::Leaf(value) => { + value.cache_reference_values(loader)?; + } + Subtree::Node(_, left_ref, right_ref) => { + left_ref.cache_reference_values(loader)?; + right_ref.cache_reference_values(loader)?; + } + } + Ok(()) + } +} + +impl BlobStoreMovable for LfmbTree { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_inner = match &self.inner { + LfmbTreeInner::Empty => LfmbTreeInner::Empty, + LfmbTreeInner::NonEmpty(size, subtree) => { + LfmbTreeInner::NonEmpty(*size, subtree.move_blob_store(from_loader, to_storer)?) + } + }; + + Ok(LfmbTree { + inner: new_inner, + _key_type: PhantomData, + }) + } +} + +impl BlobStoreMovable for Subtree { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + Ok(match self { + Subtree::Leaf(value_ref) => { + let new_value_ref = value_ref.move_blob_store(from_loader, to_storer)?; + Subtree::Leaf(new_value_ref) + } + Subtree::Node(height, left_ref, right_ref) => { + let new_left_ref = left_ref.move_blob_store(from_loader, to_storer)?; + let new_right_ref = right_ref.move_blob_store(from_loader, to_storer)?; + Subtree::Node(*height, new_left_ref, new_right_ref) + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::persistent::blob_store; + use crate::persistent::blob_store::test_stub::{BlobStoreStub, UnreachableBlobStore}; + use crate::persistent::blob_store::{BlobStoreLocation, StoreSerialized}; + use assert_matches::assert_matches; + use std::fmt::Debug; + + #[derive(Debug, Copy, Clone, Eq, PartialEq)] + struct TestKey(u64); + + type TestTree = LfmbTree>; + + impl LfmbTreeKey for TestKey { + fn to_u64(self) -> u64 { + self.0 + } + + fn from_u64(key: u64) -> Self { + Self(key) + } + } + + fn create_tree_in_memory(store: &mut impl BlobStoreLoad, size: u64) -> TestTree { + let mut tree = TestTree::empty(); + for i in 0..size { + let key; + (key, tree) = tree.insert_value(store, StoreSerialized(i + 10)).unwrap(); + assert_eq!(key, TestKey(i)); + } + tree + } + + fn store_value( + store: &mut S, + value: &T, + ) -> T { + let blob_loc = blob_store::store_to_store(store, value); + blob_store::load_from_store(store, blob_loc).unwrap() + } + + /// Test [`LfmbTree::size`] + #[test] + fn prop_test_size() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let tree = create_tree_in_memory(&mut store, i); + + // Assert size + assert_eq!(tree.size(), i, "get size for tree of size {}", i); + } + } + + /// Test [`LfmbTree::lookup_value`] for a tree that is in memory. + #[test] + fn prop_test_lookup_value_in_memory() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let tree = create_tree_in_memory(&mut store, i); + + // Lookup existing values + for j in 0..i { + assert_eq!( + tree.lookup_value(&store, TestKey(j)).unwrap().as_deref(), + Some(&StoreSerialized(j + 10)), + "access value for key {:?} in tree of size {}", + TestKey(j), + i + ); + } + + // Lookup non-existing values + assert_eq!( + tree.lookup_value(&store, TestKey(i)).unwrap(), + None, + "access non-existing value for key {:?} in tree of size {}", + TestKey(i), + i + ); + assert_eq!( + tree.lookup_value(&store, TestKey(i + 1)).unwrap(), + None, + "access non-existing value for key {:?} in tree of size {}", + TestKey(i + 1), + i + ); + } + } + + /// Test [`LfmbTree::lookup_value`] for a tree that is in blob store. + #[test] + fn prop_test_lookup_value_stored() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree and store it + let tree = create_tree_in_memory(&mut store, i); + let tree = store_value(&mut store, &tree); + + // Lookup existing values + for j in 0..i { + assert_eq!( + tree.lookup_value(&store, TestKey(j)).unwrap().as_deref(), + Some(&StoreSerialized(j + 10)), + "access value for key {:?} in tree of size {}", + TestKey(j), + i + ); + } + + // Lookup non-existing values + assert_eq!( + tree.lookup_value(&store, TestKey(i)).unwrap(), + None, + "access non-existing value for key {:?} in tree of size {}", + TestKey(i), + i + ); + assert_eq!( + tree.lookup_value(&store, TestKey(i + 1)).unwrap(), + None, + "access non-existing value for key {:?} in tree of size {}", + TestKey(i + 1), + i + ); + } + } + + /// Test [`LfmbTree::values`] for a tree that is in memory. + #[test] + fn prop_test_values_in_memory() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let tree = create_tree_in_memory(&mut store, i); + + // Iterate values + let mut values = tree.values(&store); + + // Assert values as expected + assert_eq!( + values.len(), + i as usize, + "values length for tree of size {}", + i + ); + let mut j = 0; + while let Some(entry_res) = values.next() { + let (key, val) = entry_res.unwrap(); + assert_eq!(key, TestKey(j), "key {} in tree of size {}", j, i); + assert_eq!( + *val, + StoreSerialized(j + 10), + "value number {} in tree of size {}", + j, + i + ); + j += 1; + assert_eq!(values.len(), (i - j) as usize); + } + assert_eq!(values.len(), 0); + assert_eq!(values.next().transpose().unwrap(), None); + assert_eq!(values.len(), 0); + } + } + + /// Test [`LfmbTree::values`] for a tree that is stored in blob store. + #[test] + fn prop_test_values_stored() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let tree = create_tree_in_memory(&mut store, i); + let tree = store_value(&mut store, &tree); + + // Iterate values + let mut values = tree.values(&store); + + // Assert values as expected + assert_eq!( + values.len(), + i as usize, + "values length for tree of size {}", + i + ); + let mut j = 0; + while let Some(entry_res) = values.next() { + let (key, val) = entry_res.unwrap(); + assert_eq!(key, TestKey(j), "key {} in tree of size {}", j, i); + assert_eq!( + *val, + StoreSerialized(j + 10), + "value number {} in tree of size {}", + j, + i + ); + j += 1; + assert_eq!(values.len(), (i - j) as usize); + } + assert_eq!(values.len(), 0); + assert_eq!(values.next().transpose().unwrap(), None); + assert_eq!(values.len(), 0); + } + } + + /// Test [`LfmbTree::update_value`] + #[test] + fn prop_test_update_value() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let mut tree = create_tree_in_memory(&mut store, i); + + // Update each of the values + for j in 0..i { + // Update the value + tree = tree + .update_value(&store, TestKey(j), |val| Ok(StoreSerialized(val.0 + 10))) + .expect("update existing value") + .unwrap(); + + // Lookup the value again + assert_eq!( + tree.lookup_value(&store, TestKey(j)).unwrap().as_deref(), + Some(&StoreSerialized(j + 20)), + "update value for key {:?} in tree of size {}", + TestKey(j), + i + ); + } + + // Update non-existing values + assert_matches!( + tree.update_value(&store, TestKey(i), |val| Ok(*val)) + .unwrap(), + None, + "update non-existing value for key {:?} in tree of size {}", + TestKey(i), + i + ); + assert_matches!( + tree.update_value(&store, TestKey(i + 1), |val| Ok(*val)) + .unwrap(), + None, + "update non-existing value for key {:?} in tree of size {}", + TestKey(i + 1), + i + ); + } + } + + /// Tests storing the tree into the blob store and loading it again. + #[test] + fn prop_test_store_and_load() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + + // Append values to tree + let tree1 = create_tree_in_memory(&mut store, i); + + // Store tree + let blob_ref = blob_store::store_to_store(&mut store, &tree1); + + // Load tree + let tree2: TestTree = blob_store::load_from_store(&store, blob_ref).unwrap(); + + // Assert loaded tree is equal to the tree we started with + assert_trees_eq( + &store, + &store, + &tree1, + &tree2, + format!("loaded tree of size {}", i), + ); + } + } + + /// Tests moving tree into new blob store + #[test] + fn prop_test_move_blob_store() { + for i in 0..100 { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create tree and store it + let tree = create_tree_in_memory(&mut from_store, i); + blob_store::store_to_store(&mut from_store, &tree); + + // Migrate the tree and store it + let new_tree = tree.move_blob_store(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_tree); + + // Assert migrated tree is equal to the tree we started with + assert_trees_eq( + &from_store, + &to_store, + &tree, + &new_tree, + format!("loaded tree of size {}", i), + ); + drop(new_tree); + + // Load migrated tree from destination store + let new_tree2: TestTree = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + + // Assert tree loaded from destination store is equal to the tree we started with + assert_trees_eq( + &from_store, + &to_store, + &tree, + &new_tree2, + format!("loaded tree of size {}", i), + ); + } + } + + /// Tests caching tree. + #[test] + fn prop_test_cache() { + for i in 0..100 { + let mut store = BlobStoreStub::default(); + let tree1 = create_tree_in_memory(&mut store, i); + let blob_ref = blob_store::store_to_store(&mut store, &tree1); + let tree2: TestTree = blob_store::load_from_store(&store, blob_ref).unwrap(); + + // Cache tree + tree2.cache_reference_values(&store).expect("cache"); + + // Assert cached tree is identical to the tree with started with + assert_trees_eq( + &store, + &store, + &tree1, + &tree2, + format!("cached tree of size {}", i), + ); + + // Assert that when caching again or looking up entries, we don't need to read from the blob store again. + // We assert that by using UnreachableBlobStore. + tree2 + .cache_reference_values(&UnreachableBlobStore) + .expect("cache"); + for j in 0..i { + assert_eq!( + tree2 + .lookup_value(&UnreachableBlobStore, TestKey(j)) + .unwrap() + .as_deref(), + Some(&StoreSerialized(j + 10)), + "lookup value for key {:?} in cached tree of size {}", + TestKey(j), + i + ); + } + assert_eq!( + tree2 + .lookup_value(&UnreachableBlobStore, TestKey(i)) + .unwrap(), + None, + "lookup non-existing value for key {:?} in cached tree of size {}", + TestKey(i), + i + ); + } + } + + /// Assert snapshot of hash of empty tree. + /// Hash snapshot must not change and must be equal to Haskell LFMB tree implementation. + #[test] + fn snapshot_test_hash_empty_tree() { + let store = BlobStoreStub::default(); + + let tree = LfmbTree::>::empty(); + let hash = tree.hash(&store).unwrap(); + assert_eq!( + hex::encode(hash.bytes), + "c423f9e91ee218b2b5303485dd87a3093a653ddb9bdb839d30aa1924de1dbf05" + ); + } + + /// Assert snapshot of hash of tree with 3 values A, B, C. + /// Hash snapshot must not change and must be equal to Haskell LFMB tree implementation. + #[test] + fn snapshot_test_hash_simple_tree() { + let store = BlobStoreStub::default(); + + let tree = LfmbTree::>::empty(); + let tree1 = tree + .insert_value(&store, StoreSerialized("A".to_string())) + .unwrap() + .1; + let tree2 = tree1 + .insert_value(&store, StoreSerialized("B".to_string())) + .unwrap() + .1; + let tree3 = tree2 + .insert_value(&store, StoreSerialized("C".to_string())) + .unwrap() + .1; + let hash = tree3.hash(&store).unwrap(); + assert_eq!( + hex::encode(hash.bytes), + "b9cac19f6048ef301f586e7e0faa6c08b6012d4b100703eef5dc1fcb26c1ecd5" + ); + } + + /// Load empty tree from storage bytes fixture. + /// The fixture bytes must not change and must be compatible with Haskell LFMB tree implementation. + #[test] + fn fixture_test_storage_empty_tree() { + let store = BlobStoreStub(hex::decode("00000000000000080000000000000000").unwrap()); + + let tree: LfmbTree> = + blob_store::load_from_store(&store, BlobStoreLocation(0)).expect("load tree"); + assert_eq!(tree.size(), 0); + } + + /// Load tree with 3 values A, B, C from storage bytes fixture. + /// The fixture bytes must not change and must be compatible with Haskell LFMB tree implementation. + #[test] + fn fixture_test_storage_simple_tree() { + let store = BlobStoreStub(hex::decode("0000000000000009000000000000000141000000000000000900000000000000000000000000000000090000000000000001420000000000000009000000000000000022000000000000001901000000000000000000000000000000110000000000000033000000000000000900000000000000014300000000000000090000000000000000650000000000000021000000000000000301000000000000000100000000000000440000000000000076").unwrap()); + + let tree: LfmbTree> = + blob_store::load_from_store(&store, BlobStoreLocation(135)).expect("load tree"); + assert_eq!(tree.size(), 3); + assert_eq!( + *tree.lookup_value(&store, TestKey(0)).unwrap().unwrap(), + StoreSerialized("A".to_string()) + ); + assert_eq!( + *tree.lookup_value(&store, TestKey(1)).unwrap().unwrap(), + StoreSerialized("B".to_string()) + ); + assert_eq!( + *tree.lookup_value(&store, TestKey(2)).unwrap().unwrap(), + StoreSerialized("C".to_string()) + ); + } + + /// Assert node structure and values in tree are equal. + fn assert_trees_eq( + loader1: &impl BlobStoreLoad, + loader2: &impl BlobStoreLoad, + tree1: &LfmbTree, + tree2: &LfmbTree, + context: String, + ) { + match (&tree1.inner, &tree2.inner) { + (LfmbTreeInner::Empty, LfmbTreeInner::Empty) => { + // equal + } + ( + LfmbTreeInner::NonEmpty(size1, subtree1), + LfmbTreeInner::NonEmpty(size2, subtree2), + ) => { + assert_eq!(size1, size2); + assert_subtrees_eq(loader1, loader2, subtree1, subtree2, context.clone()); + } + (_, _) => { + panic!("{}: trees not equal: {:?}, {:?}", context, tree1, tree2); + } + } + } + + /// Assert node structure and values in subtree are equal. + fn assert_subtrees_eq( + loader1: &impl BlobStoreLoad, + loader2: &impl BlobStoreLoad, + subtree1: &Subtree, + subtree2: &Subtree, + context: String, + ) { + match (subtree1, subtree2) { + (Subtree::Leaf(val_ref1), Subtree::Leaf(val_ref2)) => { + let val1 = &*val_ref1.value(loader1).unwrap(); + let val2 = &*val_ref2.value(loader2).unwrap(); + assert_eq!(val1, val2, "{}: leaf value", context); + } + ( + Subtree::Node(height1, left_ref1, right_ref1), + Subtree::Node(height2, left_ref2, right_ref2), + ) => { + assert_eq!(height1, height2); + let left1 = &*left_ref1.value(loader1).unwrap(); + let right1 = &*right_ref1.value(loader1).unwrap(); + let left2 = &*left_ref2.value(loader2).unwrap(); + let right2 = &*right_ref2.value(loader2).unwrap(); + assert_subtrees_eq(loader1, loader2, left1, left2, context.clone()); + assert_subtrees_eq(loader1, loader2, right1, right2, context.clone()); + } + (_, _) => { + panic!("subtrees not equal: {:?}, {:?}", subtree1, subtree2); + } + } + } +} diff --git a/plt/plt-block-state/src/persistent/protocol_level_locks.rs b/plt/plt-block-state/src/persistent/protocol_level_locks.rs new file mode 100644 index 0000000000..d7faebf981 --- /dev/null +++ b/plt/plt-block-state/src/persistent/protocol_level_locks.rs @@ -0,0 +1 @@ +pub mod p11; diff --git a/plt/plt-block-state/src/persistent/protocol_level_locks/p11.rs b/plt/plt-block-state/src/persistent/protocol_level_locks/p11.rs new file mode 100644 index 0000000000..562b4b769d --- /dev/null +++ b/plt/plt-block-state/src/persistent/protocol_level_locks/p11.rs @@ -0,0 +1,521 @@ +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreStore, Loadable, Storable, StoreSerialized, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::{self, Hashable}; +use crate::persistent::lfmb_tree::{LfmbTree, LfmbTreeKey}; +use crate::persistent::protocol_level_tokens::p9::TokenIndex; +use concordium_base::base::AccountIndex; +use concordium_base::common::types::TransactionTime; +use concordium_base::common::{Buffer, Serialize}; +use concordium_base::hashes::Hash; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::{CborMemo, RawCbor, TokenId}; +use std::collections::BTreeSet; +use std::io::Read; + +/// Index of the protocol-level lock in the block state map of locks. +/// +/// This type is the internal identifier of the lock in the block state and should never be exposed +/// in the API, events or used in state hashing. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +pub struct LockIndex(pub u64); + +impl LfmbTreeKey for LockIndex { + fn to_u64(self) -> u64 { + self.0 + } + + fn from_u64(key: u64) -> Self { + Self(key) + } +} + +/// Block state for protocol level locks on P11 and later protocols that uses the same representation. +#[derive(Debug, Clone, Default)] +pub struct PersistentLocksP11 { + /// Persistent map of lock index to locks. + /// + /// Here `None` represents a lock which has been deleted and acts as tombstone, preventing new + /// locks from using the same index. + pub(crate) locks: LfmbTree>, + /// Index for mapping Lock ID to the internal lock index used above. + /// + /// Deleted locks are absent from this index. + pub(crate) lock_id_map: im::HashMap, +} + +impl Loadable for PersistentLocksP11 { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let locks: LfmbTree> = + Loadable::load_from_buffer(&mut buffer, loader)?; + // To construct the full lock id to lock index map, we need to read the LFMBTree from + // the blob store. This is not ideal. If the state is to be cached after loading, we would + // rather wait until it is cached in memory before constructing the map. + let mut lock_id_map = im::HashMap::new(); + for item in locks.values(loader) { + let (lock_index, lock) = item?; + // Skip the deleted locks. + let Some(lock) = lock.as_ref() else { + continue; + }; + let conf = lock.configuration.value(loader)?; + lock_id_map.insert(conf.0.lock_id.clone(), lock_index); + } + Ok(Self { locks, lock_id_map }) + } +} + +impl Storable for PersistentLocksP11 { + fn store_to_buffer(&self, buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.locks.store_to_buffer(buffer, storer) + } +} + +impl Cacheable for PersistentLocksP11 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.locks.cache_reference_values(loader) + } +} + +impl Hashable for PersistentLocksP11 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + self.locks.hash(loader) + } +} + +/// The block state for a single protocol-level lock. +#[derive(Debug, Clone)] +pub struct PersistentLockP11 { + /// Contains references to the tokens with balances locked within this lock. + /// + /// Note the entire collection will be written to disk every time this struct is written to disk. + pub locked_balances: StoreSerialized>, + /// The configuration parameters for the lock. + pub configuration: HashedCacheableRef>, +} + +impl Loadable for PersistentLockP11 { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let locked_balances = Loadable::load_from_buffer(&mut buffer, loader)?; + let configuration = Loadable::load_from_buffer(&mut buffer, loader)?; + Ok(Self { + locked_balances, + configuration, + }) + } +} + +impl Storable for PersistentLockP11 { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.locked_balances.store_to_buffer(&mut buffer, storer); + self.configuration.store_to_buffer(&mut buffer, storer); + } +} + +impl Cacheable for PersistentLockP11 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.configuration.cache_reference_values(loader) + } +} + +impl Hashable for PersistentLockP11 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + let locked_balances = self.locked_balances.hash(loader)?; + let configuration = self.configuration.hash(loader)?; + Ok(hash::hash_of_hashes(locked_balances, configuration)) + } +} + +// Represents a list of lock recipients. This type enforces that the inner list is always sorted +// to enable binary search. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub struct LockRecipientsList { + #[size_length = 2] + recipients: Vec, +} + +impl LockRecipientsList { + /// Create a new list of lock recipients from the given account index list + pub fn new(mut recipients: Vec) -> Self { + recipients.sort(); + Self { recipients } + } + + /// Get an iterator of the account indices in the list + pub fn iter(&self) -> impl Iterator { + self.recipients.iter() + } + + /// Check whether the given account is a member + pub fn is_recipient(&self, account: &AccountIndex) -> bool { + self.recipients.binary_search(account).is_ok() + } + + /// Get the length of the list + pub fn len(&self) -> usize { + self.recipients.len() + } + + /// Check whether the list is empty + pub fn is_empty(&self) -> bool { + self.recipients.is_empty() + } +} + +/// Accounts that can receive funds from this lock in block state. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub enum LockRecipients { + /// Any eligible account can receive funds from this lock. + Any, + /// Only the listed accounts can receive funds from this lock. + Limited(LockRecipientsList), +} + +impl LockRecipients { + /// Check whether this representation allows any recipient. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Check if the given account is a recipient. + pub fn is_recipient(&self, account: &AccountIndex) -> bool { + match self { + Self::Any => true, + Self::Limited(recipients) => recipients.recipients.binary_search(account).is_ok(), + } + } +} + +impl From> for LockRecipients { + fn from(recipients: Vec) -> Self { + Self::Limited(LockRecipientsList::new(recipients)) + } +} + +/// Lock configuration at the block state level. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub struct LockConfiguration { + /// Identifier of the lock. + pub lock_id: LockId, + /// Accounts that can receive funds from this lock. + pub recipients: LockRecipients, + /// Expiry time of the lock (seconds since epoch). + pub expiry: TransactionTime, + /// Controller configuration for the lock. + pub controller: LockControllerConfig, + /// Optional raw CBOR-encoded user-facing lock metadata. + pub metadata: Option, +} + +/// Top-level lock controller type. +/// +/// Each variant represents a different controller version. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub enum LockControllerConfig { + /// SimpleV0 lock controller configuration. + SimpleV0(LockControllerSimpleV0), +} + +/// Configuration for a SimpleV0 lock controller. +/// +/// Contains the list of capability grants, which tokens are affected, +/// a keep-alive flag, and an optional memo. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub struct LockControllerSimpleV0 { + /// Capability grants to accounts. + #[size_length = 2] + pub grants: Vec, + /// Tokens affected by this lock controller. + #[size_length = 2] + // todo change to TokenIndex? + pub tokens: Vec, + /// Whether the lock should be kept alive after all funds are + /// returned. + pub keep_alive: bool, + /// Optional memo attached to the lock. + pub memo: Option, +} + +impl LockControllerSimpleV0 { + /// Check if an account has a specified role. + pub fn has_role(&self, account: AccountIndex, role: LockControllerSimpleV0Capability) -> bool { + self.grants + .iter() + .any(|grant| grant.account == account && grant.roles.contains(&role)) + } +} + +/// A grant of capabilities to a specific account for a SimpleV0 lock +/// controller. +/// +/// Each grant assigns one or more [`LockControllerSimpleV0Capability`] roles +/// to the given account, authorizing it to perform the corresponding lock +/// operations. +#[derive(Debug, Clone, Eq, PartialEq, Serialize)] +pub struct LockControllerSimpleV0Grant { + /// The account receiving the grant. + pub account: AccountIndex, + /// The capabilities granted to the account. + #[size_length = 1] + pub roles: Vec, +} + +#[cfg(test)] +mod test { + use super::*; + use concordium_base::common; + use concordium_base::transactions::Memo; + + #[test] + fn test_lock_configuration_serial() { + use concordium_base::common::types::TransactionTime; + use concordium_base::protocol_level_locks::LockControllerSimpleV0Capability; + + let lock_config = LockConfiguration { + lock_id: LockId { + account_index: 50, + sequence_number: 2, + creation_order: 0, + }, + recipients: LockRecipients::from(vec![ + AccountIndex::from(1u64), + AccountIndex::from(2u64), + ]), + expiry: TransactionTime::from(1000u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1u64), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec!["token1".parse().unwrap()], + keep_alive: true, + memo: None, + }), + metadata: None, + }; + + let bytes = common::to_bytes(&lock_config); + assert_eq!( + hex::encode(&bytes), + "0000000000000032000000000000000200000000000000000100020000000000000001000000000000000200000000000003e800000100000000000000010100000106746f6b656e31010000" + ); + + let deserialized: LockConfiguration = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, lock_config); + } + + #[test] + fn test_lock_configuration_serial_empty_recipients() { + use concordium_base::common::types::TransactionTime; + + let lock_config = LockConfiguration { + lock_id: LockId { + account_index: 50, + sequence_number: 2, + creation_order: 0, + }, + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(500u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + let bytes = common::to_bytes(&lock_config); + assert_eq!( + hex::encode(&bytes), + "00000000000000320000000000000002000000000000000001000000000000000001f40000000000000000" + ); + + let deserialized: LockConfiguration = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, lock_config); + } + + #[test] + fn test_lock_configuration_serial_with_metadata() { + use concordium_base::common::types::TransactionTime; + + let lock_config = LockConfiguration { + lock_id: LockId { + account_index: 50, + sequence_number: 2, + creation_order: 0, + }, + recipients: LockRecipients::Any, + expiry: TransactionTime::from(500u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }), + metadata: Some(RawCbor::from(vec![ + 0xa1, 0x64, b'n', b'a', b'm', b'e', 0x64, b't', b'e', b's', b't', + ])), + }; + + let bytes = common::to_bytes(&lock_config); + assert_eq!( + hex::encode(&bytes), + "0000000000000032000000000000000200000000000000000000000000000001f400000000000000010000000ba1646e616d656474657374" + ); + + let deserialized: LockConfiguration = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, lock_config); + } + + #[test] + fn test_lock_recipients_limited_sorts_accounts() { + let recipients = + LockRecipients::from(vec![AccountIndex::from(2u64), AccountIndex::from(1u64)]); + + assert_eq!( + recipients, + LockRecipients::from(vec![AccountIndex::from(1u64), AccountIndex::from(2u64)]) + ); + } + + #[test] + fn test_lock_configuration_serial_any_recipient_sentinel() { + use concordium_base::common::types::TransactionTime; + + let lock_config = LockConfiguration { + lock_id: LockId { + account_index: 50, + sequence_number: 2, + creation_order: 0, + }, + recipients: LockRecipients::Any, + expiry: TransactionTime::from(500u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + assert!(lock_config.recipients.is_any()); + assert!( + lock_config + .recipients + .is_recipient(&AccountIndex::from(0u64)) + ); + assert!( + lock_config + .recipients + .is_recipient(&AccountIndex::from(42u64)) + ); + + let bytes = common::to_bytes(&lock_config); + assert_eq!( + hex::encode(&bytes), + "0000000000000032000000000000000200000000000000000000000000000001f40000000000000000" + ); + + let deserialized: LockConfiguration = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, lock_config); + assert!(deserialized.recipients.is_any()); + } + + #[test] + fn test_lock_controller_simple_v0_grant_serial() { + let grant = LockControllerSimpleV0Grant { + account: AccountIndex::from(42u64), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Return, + ], + }; + + let bytes = common::to_bytes(&grant); + assert_eq!(hex::encode(&bytes), "000000000000002a020001"); + + let deserialized: LockControllerSimpleV0Grant = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, grant); + } + + #[test] + fn test_lock_controller_simple_v0_serial() { + let controller = LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1u64), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec!["token1".parse::().unwrap()], + keep_alive: true, + memo: Some(CborMemo::Raw( + Memo::try_from(vec![0x01, 0x02, 0x03]).unwrap(), + )), + }; + + let bytes = common::to_bytes(&controller); + assert_eq!( + hex::encode(&bytes), + "000100000000000000010100000106746f6b656e310101000003010203" + ); + + let deserialized: LockControllerSimpleV0 = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, controller); + } + + #[test] + fn test_lock_controller_simple_v0_serial_minimal() { + let controller = LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }; + + let bytes = common::to_bytes(&controller); + assert_eq!(hex::encode(&bytes), "000000000000"); + + let deserialized: LockControllerSimpleV0 = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, controller); + } + + #[test] + fn test_lock_controller_serial() { + let controller = LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1u64), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec!["token1".parse::().unwrap()], + keep_alive: true, + memo: None, + }); + + let bytes = common::to_bytes(&controller); + assert_eq!( + hex::encode(&bytes), + "00000100000000000000010100000106746f6b656e310100" + ); + + let deserialized: LockControllerConfig = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(deserialized, controller); + } +} diff --git a/plt/plt-block-state/src/persistent/protocol_level_tokens.rs b/plt/plt-block-state/src/persistent/protocol_level_tokens.rs new file mode 100644 index 0000000000..f47f7c5254 --- /dev/null +++ b/plt/plt-block-state/src/persistent/protocol_level_tokens.rs @@ -0,0 +1,17 @@ +//! Persistent model for protocol-level tokens in the block state. + +use concordium_base::protocol_level_tokens::TokenId; + +pub mod p9; + +/// Internally used, normalized token is. Used to identify token ids +/// as equal, even if casing differs. +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct NormalizedTokenId(String); + +/// Normalize the given token id. Two tokens are the same, if their +/// normalized token ids are equal. Normalizing the token id +/// removes differences due to casing. +pub fn normalize_token_id(token_id: &TokenId) -> NormalizedTokenId { + NormalizedTokenId(token_id.as_ref().to_ascii_lowercase()) +} diff --git a/plt/plt-block-state/src/persistent/protocol_level_tokens/p9.rs b/plt/plt-block-state/src/persistent/protocol_level_tokens/p9.rs new file mode 100644 index 0000000000..a0cc48c08a --- /dev/null +++ b/plt/plt-block-state/src/persistent/protocol_level_tokens/p9.rs @@ -0,0 +1,253 @@ +use crate::failure::BlockStateResult; +use crate::persistent::blob_reference::hashed_cacheable_reference::HashedCacheableRef; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreMovable, BlobStoreStore, Loadable, Storable, StoreSerialized, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use crate::persistent::lfmb_tree::{LfmbTree, LfmbTreeKey}; +use crate::persistent::protocol_level_tokens::NormalizedTokenId; +use crate::persistent::{hash, protocol_level_tokens, smart_contract_trie}; +use crate::utils::Cow; +use concordium_base::common::{Buffer, Serialize}; +use concordium_base::hashes::Hash; +use concordium_base::protocol_level_tokens::{TokenId, TokenModuleRef}; +use plt_scheduler_types::types::tokens::RawTokenAmount; +use std::io::Read; + +/// Index of the protocol-level token in the block state map of tokens. +/// +/// Corresponding Haskell type: `Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.TokenIndex` +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +pub struct TokenIndex(pub u64); + +/// Static configuration for a protocol-level token. +/// +/// Corresponding Haskell type: `Concordium.GlobalState.Persistent.BlockState.ProtocolLevelTokens.PLTConfiguration` +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +pub struct TokenConfiguration { + /// The token ID in its canonical form. Token IDs are case-insensitive when compared, + /// but the canonical token ID preserves the original casing specified when + /// the token was created. + pub token_id: TokenId, + /// The token module reference. + pub module_ref: TokenModuleRef, + /// The number of decimal places used in the representation of the token. + pub decimals: u8, +} + +/// Block state for protocol level tokens on P9 and later protocols that uses the same representation. +#[derive(Debug, Clone, Default)] +pub struct PersistentTokensP9 { + /// Persistent tree with tokens by token index. Token are never deleted. + pub(crate) tokens: LfmbTree, + /// Map for normalized token id to token index. This map is represented in memory + /// only and reconstructed each time tokens are loaded. + pub(crate) token_id_map: im::HashMap, +} + +impl Storable for PersistentTokensP9 { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.tokens.store_to_buffer(&mut buffer, storer); + } +} + +impl Loadable for PersistentTokensP9 { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + let tokens: LfmbTree = + Loadable::load_from_buffer(&mut buffer, loader)?; + // To construct the full token id to token index map, we need to read the LFMBTree from + // the blob store. This is not ideal. If the state is to be cached after loading, we would + // rather wait until it is cached in memory before constructing the map. + let token_id_map = tokens + .values(loader) + .map(|item| { + let (token_index, plt) = item?; + let conf = plt.configuration.value(loader)?; + Ok(( + protocol_level_tokens::normalize_token_id(&conf.0.token_id), + token_index, + )) + }) + .collect::>>()?; + + Ok(Self { + tokens, + token_id_map, + }) + } +} + +impl Cacheable for PersistentTokensP9 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.tokens.cache_reference_values(loader) + } +} + +impl Hashable for PersistentTokensP9 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + self.tokens.hash(loader) + } +} + +impl LfmbTreeKey for TokenIndex { + fn to_u64(self) -> u64 { + self.0 + } + + fn from_u64(key: u64) -> Self { + Self(key) + } +} + +/// Persistent protocol-level token on P9 and later protocols that uses the same representation. +#[derive(Debug, Clone)] +pub struct PersistentTokenP9 { + /// Static configuration of the token that never changes. + pub(crate) configuration: HashedCacheableRef>, + /// Dynamic key-value state for values related to the token. + pub(crate) key_value_state: HashedCacheableRef, + /// Current circulating supply of the token. + pub(crate) circulating_supply: StoreSerialized, +} + +impl<'b> Cow<'b, PersistentTokenP9> { + /// Move [`Cow`] to configuration. + pub fn cow_project_configuration( + self, + ) -> Cow<'b, HashedCacheableRef>> { + match self { + Cow::Owned(this) => Cow::Owned(this.configuration), + Cow::Borrowed(this) => Cow::Borrowed(&this.configuration), + } + } +} + +impl Storable for PersistentTokenP9 { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.configuration.store_to_buffer(&mut buffer, storer); + self.key_value_state.store_to_buffer(&mut buffer, storer); + self.circulating_supply.store_to_buffer(&mut buffer, storer); + } +} + +impl Loadable for PersistentTokenP9 { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> BlockStateResult { + let configuration = Loadable::load_from_buffer(&mut buffer, loader)?; + let key_value_state = Loadable::load_from_buffer(&mut buffer, loader)?; + let circulating_supply = Loadable::load_from_buffer(&mut buffer, loader)?; + + Ok(Self { + configuration, + key_value_state, + circulating_supply, + }) + } +} + +impl Cacheable for PersistentTokenP9 { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.configuration.cache_reference_values(loader)?; + self.key_value_state.cache_reference_values(loader)?; + Ok(()) + } +} + +impl Hashable for PersistentTokenP9 { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + let config = self.configuration.hash(loader)?; + let key_value_state = self.key_value_state.hash(loader)?; + let state = hash::hash_of_serialization((key_value_state, self.circulating_supply.0)); + + Ok(hash::hash_of_hashes(config, state)) + } +} + +impl BlobStoreMovable for PersistentTokenP9 { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_configuration = self.configuration.move_blob_store(from_loader, to_storer)?; + let new_key_value_state = self + .key_value_state + .move_blob_store(from_loader, to_storer)?; + + Ok(Self { + configuration: new_configuration, + circulating_supply: self.circulating_supply, + key_value_state: new_key_value_state, + }) + } +} + +impl BlobStoreMovable for PersistentTokensP9 { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_tokens = self.tokens.move_blob_store(from_loader, to_storer)?; + let new_token_id_map = self.token_id_map.clone(); + + Ok(Self { + tokens: new_tokens, + token_id_map: new_token_id_map, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::external::TokenAccountState; + use concordium_base::common; + use concordium_base::protocol_level_tokens::TokenModuleRef; + use plt_scheduler_types::types::tokens::RawTokenAmount; + + #[test] + fn test_token_configuration_serial() { + let token_configuration = TokenConfiguration { + token_id: "tokenid1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + + let bytes = common::to_bytes(&token_configuration); + assert_eq!( + hex::encode(&bytes), + "08746f6b656e696431050505050505050505050505050505050505050505050505050505050505050504" + ); + + let token_configuration_deserialized: TokenConfiguration = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_configuration_deserialized, token_configuration); + } + + #[test] + fn test_token_account_state_serial() { + let state = TokenAccountState { + balance: RawTokenAmount::from(10), + }; + + let bytes = common::to_bytes(&state); + assert_eq!(hex::encode(&bytes), "0a"); + + let state_deserialized: TokenAccountState = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(state_deserialized, state); + } +} diff --git a/plt/plt-block-state/src/persistent/smart_contract_trie.rs b/plt/plt-block-state/src/persistent/smart_contract_trie.rs new file mode 100644 index 0000000000..8897256c7b --- /dev/null +++ b/plt/plt-block-state/src/persistent/smart_contract_trie.rs @@ -0,0 +1,536 @@ +//! Adapter for the trie in the `concordium-smart-contract-engine` crate. There is an +//! impedance mismatch between the Rust block state and the smart contract trie, on how +//! mutability (thawing/freezing) is handled, at which level interior mutability (via locks) is implemented, +//! and the specific definitions of the blob store traits. Hence, this adapter is needed to use +//! the smart contract trie in the Rust block state. + +use crate::failure::{BlockStateFailure, BlockStateResult}; +use crate::persistent::blob_store::{ + BlobStoreLoad, BlobStoreLocation, BlobStoreMovable, BlobStoreStore, Loadable, Storable, +}; +use crate::persistent::cacheable::Cacheable; +use crate::persistent::hash::Hashable; +use concordium_base::common::Buffer; +use concordium_base::hashes::Hash; +use concordium_smart_contract_engine::v1::trie; +use std::io::Read; +use std::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +/// Immutable (persistent) trie. The internal structure may be changed via interior mutability, +/// but the entries in the trie never change. This is the frozen/persistent dual to [`MutableState`]. +#[derive(Debug)] +pub struct PersistentState(RwLock); + +impl PersistentState { + /// Create empty trie. + pub fn empty() -> Self { + Self(RwLock::new(trie::PersistentState::Empty)) + } + + /// Lookup the value in the trie for the given key and return the value. + /// Returns `None` if there is no entry for the given key. + pub fn lookup_value(&self, loader: &impl BlobStoreLoad, key: &[u8]) -> Option> { + let mut loader = LoaderAdapter(loader); + let persistent_state = self.lock_read(); + persistent_state.lookup(&mut loader, key) + } + + /// Iterate entries whose keys start with the given prefix. Returns an iterator over + /// key-value pairs. + pub fn iter_prefix<'a, L: BlobStoreLoad>( + &self, + loader: &'a L, + prefix: &[u8], + ) -> BlockStateResult, Vec)> + use<'a, L>> { + let mut loader_adapter = LoaderAdapter(loader); + let mut mutable_state = self.lock_read().thaw(); + let mut trie = mutable_state.get_inner(&mut loader_adapter).lock(); + let trie_iter = trie.iter(&mut loader_adapter, prefix).map_err(|err| { + BlockStateFailure::Invariant(format!("Error iterating values in MutableTrie: {}", err)) + })?; + + Ok(PrefixIterator { + loader, + trie: trie.clone(), + trie_iter, + }) + } + + /// Thaw the trie to make it [mutable](MutableState). + pub fn thaw(&self) -> MutableState { + let persistent_state = self.lock_read(); + MutableState { + dirty: false, + inner: Mutex::new(persistent_state.thaw()), + } + } + + fn lock_write(&self) -> RwLockWriteGuard<'_, trie::PersistentState> { + self.0.write().expect("PersistentState lock poisoned") + } + + fn lock_read(&self) -> RwLockReadGuard<'_, trie::PersistentState> { + self.0.read().expect("PersistentState lock poisoned") + } +} + +impl BlobStoreMovable for PersistentState { + fn move_blob_store( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_persistent_state = self + .lock_write() + .migrate( + &mut StorerAdapter(to_storer), + &mut LoaderAdapter(from_loader), + ) + .map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Error migrating PersistentState: {}", + err + )) + })?; + + Ok(Self(RwLock::new(new_persistent_state))) + } +} + +struct PrefixIterator<'a, L> { + loader: &'a L, + trie: trie::MutableTrie, + trie_iter: Option, +} + +impl Drop for PrefixIterator<'_, L> { + fn drop(&mut self) { + if let Some(trie_iter) = self.trie_iter.as_ref() { + self.trie.delete_iter(trie_iter); + } + } +} + +impl Iterator for PrefixIterator<'_, L> +where + L: BlobStoreLoad, +{ + type Item = (Vec, Vec); + + fn next(&mut self) -> Option { + let trie_iter = self.trie_iter.as_mut()?; + + let mut loader = LoaderAdapter(self.loader); + match self + .trie + .next(&mut loader, trie_iter, &mut trie::EmptyCounter) + { + Ok(Some(entry_id)) => { + let value = self + .trie + .with_entry(entry_id, &mut loader, |value| value.to_vec())?; + Some((trie_iter.get_key().to_vec(), value)) + } + Ok(None) => None, + Err(counter_err) => match counter_err {}, + } + } +} + +/// Mutable trie. This is the thawed/mutable dual to [`PersistentState`]. +#[derive(Debug)] +pub struct MutableState { + dirty: bool, + inner: Mutex, +} + +impl MutableState { + /// Freeze the trie to make it [persistent](PersistentState). + pub fn freeze(&mut self, loader: &impl BlobStoreLoad) -> PersistentState { + PersistentState(RwLock::new( + self.lock() + .freeze(&mut LoaderAdapter(loader), &mut trie::EmptyCollector), + )) + } + + /// Lookup the value in the trie for the given key and return the value. + /// Returns `None` if there is no entry for the given key. + pub fn lookup_value(&self, loader: &impl BlobStoreLoad, key: &[u8]) -> Option> { + let mut loader_adapter = LoaderAdapter(loader); + let mut loader = LoaderAdapter(loader); + let mut mutable_state = self.lock(); + let mut trie = mutable_state.get_inner(&mut loader_adapter).lock(); + let entry_id = trie.get_entry(&mut loader, key)?; + trie.with_entry(entry_id, &mut loader, |value| value.to_vec()) + } + + /// Iterate entries whose keys start with the given prefix. Returns an iterator over + /// key-value pairs. + pub fn iter_prefix<'a, L: BlobStoreLoad>( + &self, + loader: &'a L, + prefix: &[u8], + ) -> BlockStateResult, Vec)> + use<'a, L>> { + let mut loader_adapter = LoaderAdapter(loader); + let mut mutable_state = self.lock(); + let mut trie = mutable_state.get_inner(&mut loader_adapter).lock().clone(); + let trie_iter = trie.iter(&mut loader_adapter, prefix).map_err(|err| { + BlockStateFailure::Invariant(format!("Error iterating values in MutableTrie: {}", err)) + })?; + + Ok(PrefixIterator { + loader, + trie, + trie_iter, + }) + } + + /// Insert or update the value for the given key. If no entry exists in the trie + /// for the given key, the value is inserted. If an entry already exists + /// for the given key, the value is updated. + pub fn insert_value( + &mut self, + loader: &impl BlobStoreLoad, + key: &[u8], + value: Vec, + ) -> BlockStateResult<()> { + let mut loader = LoaderAdapter(loader); + let mut trie = self.get_mut().get_inner(&mut loader).lock(); + trie.insert(&mut loader, key, value).map_err(|err| { + BlockStateFailure::Invariant(format!("Error deleting value from MutableState: {}", err)) + })?; + drop(trie); + self.dirty = true; + Ok(()) + } + + /// Delete the value for the given key. This is a no-op, if no entry exists in the trie + /// for the given key. + pub fn delete_value( + &mut self, + loader: &impl BlobStoreLoad, + key: &[u8], + ) -> BlockStateResult<()> { + let mut loader = LoaderAdapter(loader); + let mut trie = self.get_mut().get_inner(&mut loader).lock(); + trie.delete(&mut loader, key).map_err(|err| { + BlockStateFailure::Invariant(format!("Error deleting value from MutableState: {}", err)) + })?; + drop(trie); + self.dirty = true; + Ok(()) + } + + /// If the trie has been modified since being thawed. + pub fn is_dirty(&self) -> bool { + self.dirty + } + + fn lock(&self) -> MutexGuard<'_, trie::MutableState> { + self.inner.lock().expect("MutableState lock poisoned") + } + + fn get_mut(&mut self) -> &mut trie::MutableState { + self.inner.get_mut().expect("MutableState lock poisoned") + } +} + +struct StorerAdapter<'a, S>(&'a mut S); + +impl<'a, S: BlobStoreStore> trie::BackingStoreStore for StorerAdapter<'a, S> { + fn store_raw(&mut self, data: &[u8]) -> Result { + let location = self.0.store_raw(data); + Ok(trie::Reference { + reference: location.0, + }) + } +} + +impl Storable for PersistentState { + fn store_to_buffer(&self, mut buffer: impl Buffer, storer: &mut impl BlobStoreStore) { + self.lock_write() + .store_update_buf(&mut StorerAdapter(storer), &mut buffer) + .expect("error writing PersistentState to blob store"); + } +} + +struct LoaderAdapter<'a, L>(&'a L); + +impl<'a, L: BlobStoreLoad> trie::BackingStoreLoad for LoaderAdapter<'a, L> { + type R = Vec; + + fn load_raw(&mut self, location: trie::Reference) -> trie::LoadResult { + Ok(self.0.load_raw(BlobStoreLocation(location.reference))) + } +} + +impl Loadable for PersistentState { + fn load_from_buffer( + mut buffer: impl Read, + loader: &impl BlobStoreLoad, + ) -> Result { + let persistent_state = ::load( + &mut LoaderAdapter(loader), + &mut buffer, + ) + .map_err(|load_err| { + BlockStateFailure::BlobStoreDecode(format!( + "Error loading PersistentState: {}", + load_err + )) + })?; + Ok(PersistentState(RwLock::new(persistent_state))) + } +} + +impl Cacheable for PersistentState { + fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { + self.lock_write().cache(&mut LoaderAdapter(loader)); + Ok(()) + } +} + +impl Hashable for PersistentState { + fn hash(&self, loader: &impl BlobStoreLoad) -> BlockStateResult { + Ok(Hash::from( + self.lock_write().hash(&mut LoaderAdapter(loader)).hash, + )) + } +} + +#[cfg(test)] +mod test { + use crate::persistent::blob_store; + use crate::persistent::blob_store::BlobStoreMovable; + use crate::persistent::blob_store::test_stub::{BlobStoreStub, UnreachableBlobStore}; + use crate::persistent::cacheable::Cacheable; + use crate::persistent::smart_contract_trie::PersistentState; + + #[test] + fn test_insert_delete_and_lookup() { + let state = PersistentState::empty(); + + // Insert entries + let mut mutable_state = state.thaw(); + assert!(!mutable_state.is_dirty()); + mutable_state + .insert_value(&UnreachableBlobStore, &[0, 1], vec![1, 1]) + .unwrap(); + assert!(mutable_state.is_dirty()); + mutable_state + .insert_value(&UnreachableBlobStore, &[0, 2], vec![2, 2]) + .unwrap(); + assert!(mutable_state.is_dirty()); + + // Lookup values in mutable state + assert_eq!( + mutable_state.lookup_value(&UnreachableBlobStore, &[0, 1]), + Some(vec![1, 1]) + ); + assert_eq!( + mutable_state.lookup_value(&UnreachableBlobStore, &[0, 2]), + Some(vec![2, 2]) + ); + assert_eq!( + mutable_state.lookup_value(&UnreachableBlobStore, &[0, 3]), + None + ); + + // Freeze state + let state = mutable_state.freeze(&UnreachableBlobStore); + + // Lookup values in persistent state + assert_eq!( + state.lookup_value(&UnreachableBlobStore, &[0, 1]), + Some(vec![1, 1]) + ); + assert_eq!( + state.lookup_value(&UnreachableBlobStore, &[0, 2]), + Some(vec![2, 2]) + ); + assert_eq!(state.lookup_value(&UnreachableBlobStore, &[0, 3]), None); + + // Update and delete entries + let mut mutable_state = state.thaw(); + mutable_state + .delete_value(&UnreachableBlobStore, &[0, 2]) + .unwrap(); + assert!(mutable_state.is_dirty()); + mutable_state + .insert_value(&UnreachableBlobStore, &[0, 1], vec![4, 4]) + .unwrap(); + assert!(mutable_state.is_dirty()); + + // Lookup values in mutable state + assert_eq!( + mutable_state.lookup_value(&UnreachableBlobStore, &[0, 1]), + Some(vec![4, 4]) + ); + assert_eq!( + mutable_state.lookup_value(&UnreachableBlobStore, &[0, 2]), + None + ); + + // Freeze state + let state = mutable_state.freeze(&UnreachableBlobStore); + + // Lookup values in persistent state + assert_eq!( + state.lookup_value(&UnreachableBlobStore, &[0, 1]), + Some(vec![4, 4]) + ); + assert_eq!(state.lookup_value(&UnreachableBlobStore, &[0, 2]), None); + } + + #[test] + fn test_iter_prefix() { + let state = PersistentState::empty(); + + // Insert entries + let mut mutable_state = state.thaw(); + mutable_state + .insert_value(&UnreachableBlobStore, &[0, 1], vec![1, 1]) + .unwrap(); + mutable_state + .insert_value(&UnreachableBlobStore, &[0, 2], vec![2, 2]) + .unwrap(); + mutable_state + .insert_value(&UnreachableBlobStore, &[1, 1], vec![3, 3]) + .unwrap(); + mutable_state + .insert_value(&UnreachableBlobStore, &[1, 2], vec![4, 4]) + .unwrap(); + mutable_state + .insert_value(&UnreachableBlobStore, &[2, 1], vec![5, 5]) + .unwrap(); + + // Iterate values in mutable state + let values: Vec<_> = mutable_state + .iter_prefix(&UnreachableBlobStore, &[0, 2]) + .unwrap() + .collect(); + assert_eq!(values, vec![(vec![0, 2], vec![2, 2])]); + let values: Vec<_> = mutable_state + .iter_prefix(&UnreachableBlobStore, &[1]) + .unwrap() + .collect(); + assert_eq!( + values, + vec![(vec![1, 1], vec![3, 3]), (vec![1, 2], vec![4, 4])] + ); + let values: Vec<_> = mutable_state + .iter_prefix(&UnreachableBlobStore, &[3]) + .unwrap() + .collect(); + assert_eq!(values, vec![]); + + // Freeze state + let state = mutable_state.freeze(&UnreachableBlobStore); + + // Iterate values in persistent state + let values: Vec<_> = state + .iter_prefix(&UnreachableBlobStore, &[0, 2]) + .unwrap() + .collect(); + assert_eq!(values, vec![(vec![0, 2], vec![2, 2])]); + let values: Vec<_> = state + .iter_prefix(&UnreachableBlobStore, &[1]) + .unwrap() + .collect(); + assert_eq!( + values, + vec![(vec![1, 1], vec![3, 3]), (vec![1, 2], vec![4, 4])] + ); + let values: Vec<_> = state + .iter_prefix(&UnreachableBlobStore, &[3]) + .unwrap() + .collect(); + assert_eq!(values, vec![]); + } + + #[test] + fn test_store_load_and_cache() { + let mut store = BlobStoreStub::default(); + let state = PersistentState::empty(); + + // Insert entries + let mut mutable_state = state.thaw(); + mutable_state + .insert_value(&store, &[0, 1], vec![1, 1]) + .unwrap(); + mutable_state + .insert_value(&store, &[0, 2], vec![2, 2]) + .unwrap(); + let state = mutable_state.freeze(&store); + + // Store trie + let blob_ref = blob_store::store_to_store(&mut store, state); + + // Load trie + let state: PersistentState = blob_store::load_from_store(&store, blob_ref).unwrap(); + + // Lookup values + assert_eq!(state.lookup_value(&store, &[0, 1]), Some(vec![1, 1])); + assert_eq!(state.lookup_value(&store, &[0, 2]), Some(vec![2, 2])); + + // Cache trie + state.cache_reference_values(&store).unwrap(); + + // Lookup values using "unreachable" blob store since entries + // should be in memory now. + assert_eq!( + state.lookup_value(&UnreachableBlobStore, &[0, 1]), + Some(vec![1, 1]) + ); + assert_eq!( + state.lookup_value(&UnreachableBlobStore, &[0, 2]), + Some(vec![2, 2]) + ); + } + + #[test] + fn test_move_blob_store() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create tree and store it + let state = PersistentState::empty(); + let mut mutable_state = state.thaw(); + mutable_state + .insert_value(&from_store, &[0, 1], vec![1, 1]) + .unwrap(); + mutable_state + .insert_value(&from_store, &[0, 2], vec![2, 2]) + .unwrap(); + let state = mutable_state.freeze(&from_store); + blob_store::store_to_store(&mut from_store, &state); + + // Move trie to new store + let new_state = state.move_blob_store(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_state); + drop(state); + + // Lookup values in migrated state + assert_eq!(new_state.lookup_value(&to_store, &[0, 1]), Some(vec![1, 1])); + assert_eq!(new_state.lookup_value(&to_store, &[0, 2]), Some(vec![2, 2])); + drop(new_state); + + // Load moved state from destination store + let new_state2: PersistentState = + blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + + // Lookup values using "unreachable" blob store since entries + // should be in memory now. + assert_eq!( + new_state2.lookup_value(&to_store, &[0, 1]), + Some(vec![1, 1]) + ); + assert_eq!( + new_state2.lookup_value(&to_store, &[0, 2]), + Some(vec![2, 2]) + ); + } +} diff --git a/plt/plt-block-state/src/utils.rs b/plt/plt-block-state/src/utils.rs new file mode 100644 index 0000000000..0213aeb020 --- /dev/null +++ b/plt/plt-block-state/src/utils.rs @@ -0,0 +1,72 @@ +//! Block state utility types and functions. + +use concordium_base::common::cbor; +use concordium_base::common::cbor::{ + CborDeserialize, CborSerializationResult, SerializationOptions, UnknownMapKeys, +}; +use std::ops::Deref; + +/// Value of type `T` that is either owned or borrowed. +/// +/// We use our own type instead of [`std::borrow::Cow`], since we don't want to require +/// `T` to implement `Clone` which `Cow` does. +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum Cow<'a, T> { + /// Value is owned + Owned(T), + /// Value is borrowed + Borrowed(&'a T), +} + +impl<'a, T> Cow<'a, T> { + /// Convert the possibly owned value into an owned value, by cloning + /// if the value is represented by a reference. + pub fn into_owned(self) -> T + where + T: Clone, + { + match self { + Self::Owned(v) => v, + Self::Borrowed(r) => r.clone(), + } + } + + /// Acquires a mutable reference to the owned form of the data. + /// + /// Clones the data if it is not already owned. + pub fn to_mut(&mut self) -> &mut T + where + T: Clone, + { + match *self { + Self::Borrowed(borrowed) => { + *self = Self::Owned(borrowed.clone()); + match *self { + Self::Borrowed(..) => unreachable!(), + Self::Owned(ref mut owned) => owned, + } + } + Self::Owned(ref mut owned) => owned, + } + } +} + +impl Deref for Cow<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + Cow::Owned(v) => v, + Cow::Borrowed(r) => r, + } + } +} + +/// Decode given CBOR using decode options set to suit the token module. The decode options +/// will generally be strict. +pub fn cbor_decode(cbor: impl AsRef<[u8]>) -> CborSerializationResult { + let decode_options = SerializationOptions { + unknown_map_keys: UnknownMapKeys::Fail, + }; + cbor::cbor_decode_with_options(cbor, decode_options) +} diff --git a/plt/plt-block-state/tests/block_state_p11.rs b/plt/plt-block-state/tests/block_state_p11.rs new file mode 100644 index 0000000000..a4298110bb --- /dev/null +++ b/plt/plt-block-state/tests/block_state_p11.rs @@ -0,0 +1,508 @@ +//! Tests of the P11 block state. + +use concordium_base::base::AccountIndex; +use concordium_base::common::types::TransactionTime; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::{ + CborMemo, RawCbor, TokenAdminRole, TokenId, TokenModuleRef, +}; +use concordium_base::transactions::Memo; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::entity::protocol_level_tokens::p11::Roles; +use plt_block_state::persistent::protocol_level_locks::p11::{ + LockConfiguration, LockControllerConfig, LockControllerSimpleV0, LockControllerSimpleV0Grant, + LockRecipients, +}; +use plt_block_state::persistent::protocol_level_tokens::p9::{TokenConfiguration, TokenIndex}; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Test create a token in the block state and read its configuration. +#[test] +fn test_create_plt() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create token + let configuration = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Read configuration + let read_configuration = block_state + .token_by_index(&context, token_index) + .unwrap() + .token_p9_base + .token_configuration(&context) + .unwrap(); + assert_eq!(read_configuration, configuration); +} + +/// Test getting list of tokens. +#[test] +fn test_plt_list() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Read empty PLT list + let tokens = block_state.plt_list(&context).unwrap().to_vec(); + assert_eq!(tokens, vec![]); + + // Create token 1 + let token_id1: TokenId = "token1".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id1.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Create token 2 + let token_id2: TokenId = "token2".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id2.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Read PLT list + let tokens = block_state.plt_list(&context).unwrap().to_vec(); + assert_eq!(tokens, vec![token_id1, token_id2]); +} + +/// Test getting token by id. +#[test] +fn test_token_by_id() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create token + let token_id1: TokenId = "token1".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id1.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Get token by id + let token_by_id = block_state + .token_by_id(&context, &token_id1) + .unwrap() + .expect("token should exist"); + assert_eq!(token_by_id.token_p9_base.token_index(), token_index); + + // Get token by non-canonical id + let non_canonical_token_id1: TokenId = "TOKEN1".parse().unwrap(); + let token_index_by_id = block_state + .token_by_id(&context, &non_canonical_token_id1) + .unwrap() + .expect("token should exist"); + assert_eq!(token_index_by_id.token_p9_base.token_index(), token_index); + + // Get non-existing token by id + let token_id2 = "token2".parse().unwrap(); + let err = block_state + .token_by_id(&context, &token_id2) + .unwrap() + .expect_err("token should not exist"); + assert_eq!(err.0, token_id2); +} + +/// Test set and get token properties stored in the key-value state. +#[test] +fn test_token_properties() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create token + let configuration = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + let mut token = block_state.token_by_index(&context, token_index).unwrap(); + + // Assert initial values + let account_index1 = AccountIndex::from(1); + let lock_id = LockId { + account_index: 7, + sequence_number: 11, + creation_order: 3, + }; + assert_eq!( + token.get_account_roles(&context, account_index1).unwrap(), + Roles::none() + ); + assert_eq!(token.all_roles(&context).unwrap(), vec![]); + assert_eq!( + token + .get_locked_balance_for_account(&context, account_index1, &lock_id) + .unwrap(), + RawTokenAmount::from(0) + ); + assert_eq!( + token + .get_locked_balances_for_account(&context, account_index1) + .unwrap(), + vec![] + ); + + // Set values + token + .assign_account_roles( + &context, + account_index1, + &[TokenAdminRole::Burn, TokenAdminRole::Mint], + ) + .unwrap(); + token + .set_locked_balance_for_account( + &context, + account_index1, + &lock_id, + RawTokenAmount::from(100), + ) + .unwrap(); + + // Update token + block_state.update_token(&context, token).unwrap(); + + // Read values + let mut token = block_state.token_by_index(&context, token_index).unwrap(); + let mut expected_roles = Roles::none(); + expected_roles.assign(TokenAdminRole::Mint); + expected_roles.assign(TokenAdminRole::Burn); + assert_eq!( + token.get_account_roles(&context, account_index1).unwrap(), + expected_roles + ); + assert_eq!( + token.all_roles(&context).unwrap(), + vec![(account_index1, expected_roles)] + ); + assert_eq!( + token + .get_locked_balance_for_account(&context, account_index1, &lock_id) + .unwrap(), + RawTokenAmount::from(100) + ); + assert_eq!( + token + .get_locked_balances_for_account(&context, account_index1) + .unwrap(), + vec![(lock_id.clone(), RawTokenAmount::from(100))] + ); + + // Update values + token + .revoke_account_roles(&context, account_index1, &[TokenAdminRole::Mint]) + .unwrap(); + token + .set_locked_balance_for_account(&context, account_index1, &lock_id, RawTokenAmount::from(0)) + .unwrap(); + + // Update token + block_state.update_token(&context, token).unwrap(); + + // Read values + let token = block_state.token_by_index(&context, token_index).unwrap(); + let mut expected_roles = Roles::none(); + expected_roles.assign(TokenAdminRole::Burn); + assert_eq!( + token.get_account_roles(&context, account_index1).unwrap(), + expected_roles + ); + assert_eq!( + token.all_roles(&context).unwrap(), + vec![(account_index1, expected_roles)] + ); + assert_eq!( + token + .get_locked_balance_for_account(&context, account_index1, &lock_id) + .unwrap(), + RawTokenAmount::from(0) + ); + assert_eq!( + token + .get_locked_balances_for_account(&context, account_index1) + .unwrap(), + vec![] + ); +} + +/// Test create a lock in the block state and read its configuration. +#[test] +fn test_create_lock() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create lock + let lock_id = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let metadata = RawCbor::from(vec![0xa1]); // The node does not care what is in the metadata + let configuration = LockConfiguration { + lock_id: lock_id.clone(), + recipients: LockRecipients::from(vec![AccountIndex::from(1), AccountIndex::from(2)]), + expiry: TransactionTime::from(100u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: AccountIndex::from(1), + roles: vec![ + LockControllerSimpleV0Capability::Cancel, + LockControllerSimpleV0Capability::Fund, + ], + }], + tokens: vec!["tokenid1".parse().unwrap(), "tokenid2".parse().unwrap()], + keep_alive: true, + memo: Some(CborMemo::Raw(Memo::try_from(vec![0, 1]).unwrap())), + }), + metadata: Some(metadata), + }; + + block_state + .create_lock(&context, configuration.clone()) + .unwrap(); + + // Read configuration + let read_configuration = block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .unwrap() + .lock_configuration(&context) + .unwrap() + .into_owned(); + assert_eq!(read_configuration, configuration); +} + +/// Test getting lock by id. +#[test] +fn test_lock_by_id() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create lock + let lock_id = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration = LockConfiguration { + lock_id: lock_id.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + block_state.create_lock(&context, configuration).unwrap(); + + // Get lock by id + let lock = block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .expect("lock should exist"); + assert_eq!( + &lock.lock_configuration(&context).unwrap().lock_id, + &lock_id + ); + + // Get non-existing lock by id + let non_existing_lock_id = LockId { + account_index: 1, + sequence_number: 2, + creation_order: 0, + }; + + block_state + .lock_by_id(&context, &non_existing_lock_id) + .unwrap() + .expect_err("lock should not exist"); +} + +/// Test set and get lock balance refs +#[test] +fn test_lock_balance_refs() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create lock + let lock_id = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration = LockConfiguration { + lock_id: lock_id.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + block_state.create_lock(&context, configuration).unwrap(); + let mut lock = block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .expect("lock should exist"); + + // Assert no initial balance refs + assert_eq!(lock.lock_balance_refs(), vec![]); + + // Add balance refs + lock.add_lock_balance_ref(AccountIndex::from(0), TokenIndex(0)); + lock.add_lock_balance_ref(AccountIndex::from(1), TokenIndex(1)); + + // Update lock + block_state.update_lock(&context, lock).unwrap(); + + // Read balance refs + let lock = block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .expect("lock should exist"); + assert_eq!( + lock.lock_balance_refs(), + vec![ + (AccountIndex::from(0), TokenIndex(0)), + (AccountIndex::from(1), TokenIndex(1)) + ] + ); +} + +/// Test creating a lock then deleting it. +#[test] +fn test_create_and_delete_lock() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Create lock + let lock_id = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration = LockConfiguration { + lock_id: lock_id.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + block_state.create_lock(&context, configuration).unwrap(); + + // Verify lock exists + block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .expect("lock should exist after creation"); + + // Delete lock + let was_deleted = block_state.delete_lock(&context, &lock_id).unwrap(); + assert!( + was_deleted, + "delete_lock should return true for an existing lock" + ); + + // Verify lock no longer exists + block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .expect_err("lock should not exist after deletion"); + + // Deleting again should return false + let was_deleted_again = block_state.delete_lock(&context, &lock_id).unwrap(); + assert!( + !was_deleted_again, + "delete_lock should return false for a non-existing lock" + ); +} + +/// Test getting list of locks. Mirrors `test_plt_list` for the lock side of the block state. +#[test] +fn test_lock_list() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP11::default(); + + // Read empty lock list + let locks = block_state.lock_list(&context).unwrap(); + assert_eq!(locks, vec![]); + + // Create locks + let lock_id_a = LockId { + account_index: 1, + sequence_number: 1, + creation_order: 0, + }; + let configuration_a = LockConfiguration { + lock_id: lock_id_a.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + + let lock_id_b = LockId { + account_index: 2, + sequence_number: 7, + creation_order: 0, + }; + let configuration_b = LockConfiguration { + lock_id: lock_id_b.clone(), + recipients: LockRecipients::from(vec![]), + expiry: TransactionTime::from(0u64), + controller: LockControllerConfig::SimpleV0(LockControllerSimpleV0 { + grants: Vec::new(), + tokens: Vec::new(), + keep_alive: false, + memo: None, + }), + metadata: None, + }; + block_state.create_lock(&context, configuration_a).unwrap(); + block_state.create_lock(&context, configuration_b).unwrap(); + + // Read lock list and sort for a stable comparison (lock_list order is not guaranteed). + let mut locks = block_state.lock_list(&context).unwrap(); + locks.sort(); + assert_eq!(locks, vec![lock_id_a, lock_id_b]); +} diff --git a/plt/plt-block-state/tests/block_state_p9.rs b/plt/plt-block-state/tests/block_state_p9.rs new file mode 100644 index 0000000000..c6b88eb762 --- /dev/null +++ b/plt/plt-block-state/tests/block_state_p9.rs @@ -0,0 +1,283 @@ +//! Tests of the P9 block state. + +use concordium_base::base::AccountIndex; +use concordium_base::protocol_level_tokens::{MetadataUrl, TokenId, TokenModuleRef}; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Test create a token in the block state and read its configuration. +#[test] +fn test_create_plt() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create token + let configuration = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Read configuration + let read_configuration = block_state + .token_by_index(&context, token_index) + .unwrap() + .token_p9_base + .token_configuration(&context) + .unwrap(); + assert_eq!(read_configuration, configuration); +} + +/// Test getting list of tokens. +#[test] +fn test_plt_list() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create token 1 + let token_id1: TokenId = "token1".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id1.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Create token 2 + let token_id2: TokenId = "token2".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id2.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Read PLT list + let tokens: Vec<_> = block_state + .plt_list(&context) + .map(|res| res.unwrap()) + .collect(); + assert_eq!(tokens, vec![token_id1, token_id2]); +} + +/// Test getting token by id. +#[test] +fn test_token_by_id() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create token + let token_id1: TokenId = "token1".parse().unwrap(); + let configuration = TokenConfiguration { + token_id: token_id1.clone(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + + // Get token by id + let token_by_id = block_state + .token_by_id(&context, &token_id1) + .unwrap() + .expect("token should exist"); + assert_eq!(token_by_id.token_p9_base.token_index(), token_index); + + // Get token by non-canonical id + let non_canonical_token_id1: TokenId = "TOKEN1".parse().unwrap(); + let token_index_by_id = block_state + .token_by_id(&context, &non_canonical_token_id1) + .unwrap() + .expect("token should exist"); + assert_eq!(token_index_by_id.token_p9_base.token_index(), token_index); + + // Get non-existing token by id + let token_id2 = "token2".parse().unwrap(); + let err = block_state + .token_by_id(&context, &token_id2) + .unwrap() + .expect_err("token should not exist"); + assert_eq!(err.0, token_id2); +} + +/// Test set and read circulating supply +#[test] +fn test_circulating_supply() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create token + let configuration = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + let mut token = block_state.token_by_index(&context, token_index).unwrap(); + + // Assert initially 0 + let circulating_supply = token.token_p9_base.token_circulating_supply(); + assert_eq!(circulating_supply, RawTokenAmount::from(0)); + + // Set supply + token + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(10)); + + // Update token + block_state.update_token(&context, token).unwrap(); + + // Read supply + let token = block_state.token_by_index(&context, token_index).unwrap(); + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(10) + ); +} + +/// Test set and get token properties stored in the key-value state. +#[test] +fn test_token_properties() { + let context = entity_test_stub::new_no_external_context(); + let mut block_state = BlockStateP9::default(); + + // Create token + let configuration = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token_index = block_state + .create_token(&context, configuration.clone()) + .unwrap(); + let mut token = block_state.token_by_index(&context, token_index).unwrap(); + + // Assert initial values + assert!(!token.token_p9_base.has_deny_list(&context)); + assert!(!token.token_p9_base.has_allow_list(&context)); + assert!(!token.token_p9_base.is_mintable(&context)); + assert!(!token.token_p9_base.is_burnable(&context)); + assert!(!token.token_p9_base.is_paused(&context)); + let account_index1 = AccountIndex::from(1); + assert!( + !token + .token_p9_base + .get_allow_list_for(&context, account_index1) + ); + assert!( + !token + .token_p9_base + .get_deny_list_for(&context, account_index1) + ); + + // Set values + token + .token_p9_base + .set_token_circulating_supply(RawTokenAmount::from(10)); + token.token_p9_base.set_deny_list_enabled(&context).unwrap(); + token + .token_p9_base + .set_allow_list_enabled(&context) + .unwrap(); + token.token_p9_base.set_mintable_enabled(&context).unwrap(); + token.token_p9_base.set_burnable_enabled(&context).unwrap(); + token.token_p9_base.set_paused(&context, true).unwrap(); + token + .token_p9_base + .set_token_name(&context, "token1") + .unwrap(); + let gov_account_index = AccountIndex::from(10); + token + .token_p9_base + .set_governance_account(&context, gov_account_index) + .unwrap(); + token + .token_p9_base + .set_allow_list_for(&context, account_index1, true) + .unwrap(); + token + .token_p9_base + .set_deny_list_for(&context, account_index1, true) + .unwrap(); + let metadata_url = MetadataUrl::from("http://test".to_string()); + token + .token_p9_base + .set_metadata_url(&context, &metadata_url) + .unwrap(); + + // Update token + block_state.update_token(&context, token).unwrap(); + + // Read values + let mut token = block_state.token_by_index(&context, token_index).unwrap(); + assert!(token.token_p9_base.has_deny_list(&context)); + assert!(token.token_p9_base.has_allow_list(&context)); + assert!(token.token_p9_base.is_mintable(&context)); + assert!(token.token_p9_base.is_burnable(&context)); + assert!(token.token_p9_base.is_paused(&context)); + assert_eq!( + token.token_p9_base.get_token_name(&context).unwrap(), + "token1" + ); + assert_eq!( + token + .token_p9_base + .get_governance_account_index(&context) + .unwrap(), + gov_account_index + ); + assert!( + token + .token_p9_base + .get_allow_list_for(&context, account_index1) + ); + assert!( + token + .token_p9_base + .get_deny_list_for(&context, account_index1) + ); + assert_eq!( + token.token_p9_base.get_metadata(&context).unwrap(), + metadata_url + ); + + // Update values + token.token_p9_base.set_paused(&context, false).unwrap(); + token + .token_p9_base + .set_allow_list_for(&context, account_index1, false) + .unwrap(); + token + .token_p9_base + .set_deny_list_for(&context, account_index1, false) + .unwrap(); + + // Update token + block_state.update_token(&context, token).unwrap(); + + // Read values + let token = block_state.token_by_index(&context, token_index).unwrap(); + assert!(!token.token_p9_base.is_paused(&context)); + assert!( + !token + .token_p9_base + .get_allow_list_for(&context, account_index1) + ); + assert!( + !token + .token_p9_base + .get_deny_list_for(&context, account_index1) + ); +} diff --git a/plt/plt-scheduler-types/Cargo.toml b/plt/plt-scheduler-types/Cargo.toml new file mode 100644 index 0000000000..da22da63db --- /dev/null +++ b/plt/plt-scheduler-types/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "plt-scheduler-types" +version = "0.1.0" +edition = "2024" + +[dependencies] +# Concordium dependencies +concordium_base.workspace = true + +# Third party dependencies +thiserror.workspace = true + +[dev-dependencies] +hex.workspace = true +proptest.workspace = true \ No newline at end of file diff --git a/plt/plt-scheduler-types/src/lib.rs b/plt/plt-scheduler-types/src/lib.rs new file mode 100644 index 0000000000..3b98985587 --- /dev/null +++ b/plt/plt-scheduler-types/src/lib.rs @@ -0,0 +1,10 @@ +//! Types that are externally exposed by the PLT Scheduler +//! as part of protocol execution and queries. +//! The types generally follow the same model +//! as the similar types on the Haskell side and implements the same serialization when possible. +//! +//! Notice that protocol types that are exposed outside the node +//! are defined in concordium-smart-contracts-common and concordium_base. As such, +//! the present crate defines types used internally in the node for protocol execution and queries. + +pub mod types; diff --git a/plt/plt-scheduler-types/src/types.rs b/plt/plt-scheduler-types/src/types.rs new file mode 100644 index 0000000000..ef134e51eb --- /dev/null +++ b/plt/plt-scheduler-types/src/types.rs @@ -0,0 +1,8 @@ +//! Types used in the externally facing API for the scheduler + +pub mod events; +pub mod execution; +pub mod protocol_version; +pub mod queries; +pub mod reject_reasons; +pub mod tokens; diff --git a/plt/plt-scheduler-types/src/types/events.rs b/plt/plt-scheduler-types/src/types/events.rs new file mode 100644 index 0000000000..30781868c5 --- /dev/null +++ b/plt/plt-scheduler-types/src/types/events.rs @@ -0,0 +1,435 @@ +//! Events produced by block items executed by the scheduler. +//! Events generally represents observable changes to the chain state. + +use crate::types::tokens::{TokenAmount, TokenHolder}; +use concordium_base::common::{Buffer, Put, Serial}; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::{RawCbor, TokenId, TokenModuleCborTypeDiscriminator}; +use concordium_base::transactions::Memo; +use concordium_base::updates::CreatePlt; + +/// Block item event. This is an observable effect on the token state. +/// +/// Corresponding Haskell type: `Concordium.Types.Execution.Event` +#[derive(Debug, Clone, PartialEq)] +pub enum BlockItemEvent { + /// An event emitted by the token module. + TokenModule(EncodedTokenModuleEvent), + /// An event emitted when a transfer of tokens is performed. + TokenTransfer(TokenTransferEvent), + /// An event emitted when the token supply is updated by minting tokens to a + /// token holder. + TokenMint(TokenMintEvent), + /// An event emitted when the token supply is updated by burning tokens from + /// the balance of a token holder. + TokenBurn(TokenBurnEvent), + /// A new token was created. + TokenCreated(TokenCreateEvent), + /// A protocol-level lock was created. + LockCreated(LockCreateEvent), + /// A protocol-level lock was destroyed + LockDestroyed(LockDestroyEvent), +} + +impl Serial for BlockItemEvent { + fn serial(&self, out: &mut B) { + match self { + BlockItemEvent::TokenModule(token_module_event) => { + out.put(&38u8); + out.put(token_module_event); + } + BlockItemEvent::TokenTransfer(token_transfer) => { + out.put(&39u8); + out.put(token_transfer); + } + BlockItemEvent::TokenMint(token_mint) => { + out.put(&40u8); + out.put(token_mint); + } + BlockItemEvent::TokenBurn(token_burn) => { + out.put(&41u8); + out.put(token_burn); + } + BlockItemEvent::TokenCreated(token_created) => { + out.put(&42u8); + out.put(token_created); + } + BlockItemEvent::LockCreated(lock_created) => { + out.put(&43u8); + out.put(lock_created); + } + BlockItemEvent::LockDestroyed(lock_destroyed) => { + out.put(&44u8); + out.put(lock_destroyed); + } + } + } +} + +/// An event emitted when a transfer of tokens from `from` to `to` is performed. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct TokenTransferEvent { + /// The canonical token id. + pub token_id: TokenId, + /// The token holder from which the tokens are transferred. + pub from: TokenHolder, + /// The token holder to which the tokens are transferred. + pub to: TokenHolder, + /// The amount of tokens transferred. + pub amount: TokenAmount, + /// An optional memo field that can be used to attach a message to the token + /// transfer. + pub memo: Option, + /// When the funds originate on the locked balance of an account, the + /// identity of the lock controlling the funds. Absent when the funds + /// are not on the locked balance of the originating account. + pub from_lock: Option, + /// When the funds are transferred into the control of a lock, the + /// identity of the lock assuming control of the funds. Absent when the + /// funds are sent to the available balance of the receiving account. + pub to_lock: Option, +} + +/// Serial implementation matching the serialization of `TokenTransfer` in `Event` +/// in the Haskell module `Concordium.Types.Execution`. +impl Serial for TokenTransferEvent { + fn serial(&self, out: &mut B) { + let set_if = |n, b| if b { 1u16 << n } else { 0 }; + let bitmap: u16 = set_if(0, self.memo.is_some()) + | set_if(1, self.from_lock.is_some()) + | set_if(2, self.to_lock.is_some()); + out.put(&bitmap); + + out.put(&self.token_id); + out.put(&self.from); + out.put(&self.to); + out.put(&self.amount); + if let Some(memo) = &self.memo { + out.put(memo); + } + if let Some(from_lock) = &self.from_lock { + out.put(from_lock); + } + if let Some(to_lock) = &self.to_lock { + out.put(to_lock); + } + } +} + +/// An event emitted when the token supply is updated by minting tokens to a +/// token holder. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct TokenMintEvent { + /// The canonical token id. + pub token_id: TokenId, + /// The account whose balance the amount is minted to. + pub target: TokenHolder, + /// The minted amount + pub amount: TokenAmount, +} + +/// Serial implementation matching the serialization of `TokenMint` in `Event` +/// in the Haskell module `Concordium.Types.Execution`. +impl Serial for TokenMintEvent { + fn serial(&self, out: &mut B) { + let bitmap: u16 = 0; + out.put(&bitmap); + + out.put(&self.token_id); + out.put(&self.target); + out.put(&self.amount); + } +} + +/// An event emitted when the token supply is updated by burning tokens from +/// the balance of a token holder. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct TokenBurnEvent { + /// The canonical token id. + pub token_id: TokenId, + /// The account whose balance the amount is burned from. + pub target: TokenHolder, + /// The burned amount + pub amount: TokenAmount, +} + +/// Serial implementation matching the serialization of `TokenBurn` in `Event` +/// in the Haskell module `Concordium.Types.Execution`. +impl Serial for TokenBurnEvent { + fn serial(&self, out: &mut B) { + let bitmap: u16 = 0; + out.put(&bitmap); + + out.put(&self.token_id); + out.put(&self.target); + out.put(&self.amount); + } +} + +/// A new token was created. +#[derive(Debug, Clone, PartialEq, Serial)] +pub struct TokenCreateEvent { + /// The update instruction payload for the token creation. + pub payload: CreatePlt, +} + +/// Event produced from the effect of a token transaction. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serial)] +pub struct EncodedTokenModuleEvent { + /// The canonical token id. + pub token_id: TokenId, + /// The type of event produced. + pub event_type: TokenModuleCborTypeDiscriminator, + /// The details of the event produced, in the raw byte encoded form. + pub details: RawCbor, +} + +#[derive(Debug, Clone, PartialEq, Serial)] +pub struct LockCreateEvent { + /// The Lock ID of the newly-created lock. + pub lock_id: LockId, + /// The CBOR-encoded lock configuration. + pub lock_config: RawCbor, +} + +#[derive(Debug, Clone, PartialEq, Serial)] +pub struct LockDestroyEvent { + /// The Lock ID of the destroyed lock. + pub lock_id: LockId, +} + +#[cfg(test)] +mod test { + use crate::types::events::{ + BlockItemEvent, EncodedTokenModuleEvent, LockCreateEvent, LockDestroyEvent, TokenBurnEvent, + TokenCreateEvent, TokenMintEvent, TokenTransferEvent, + }; + use crate::types::tokens::{RawTokenAmount, TokenAmount, TokenHolder}; + use concordium_base::common; + use concordium_base::contracts_common::AccountAddress; + use concordium_base::protocol_level_locks::LockId; + use concordium_base::protocol_level_tokens::{RawCbor, TokenModuleRef}; + use concordium_base::transactions::Memo; + use concordium_base::updates::CreatePlt; + + #[test] + fn test_token_module_event_serial() { + let event = BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: "tokenid1".parse().unwrap(), + event_type: "type1".parse().unwrap(), + details: RawCbor::from(vec![1, 2, 3, 4]), + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "2608746f6b656e6964310574797065310000000401020304" + ); + } + + #[test] + fn test_token_transfer_event_serial() { + // no memo + let event = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: "tokenid1".parse().unwrap(), + from: TokenHolder::Account(AccountAddress([1; 32])), + to: TokenHolder::Account(AccountAddress([2; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + memo: None, + from_lock: None, + to_lock: None, + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "27000008746f6b656e696431000101010101010101010101010101010101010101010101010101010101010101000202020202020202020202020202020202020202020202020202020202020202876804" + ); + + // with memo + let reject_reason = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: "tokenid1".parse().unwrap(), + from: TokenHolder::Account(AccountAddress([1; 32])), + to: TokenHolder::Account(AccountAddress([2; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + memo: Some(Memo::try_from(vec![1, 2, 3]).unwrap()), + from_lock: None, + to_lock: None, + }); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "27000108746f6b656e6964310001010101010101010101010101010101010101010101010101010101010101010002020202020202020202020202020202020202020202020202020202020202028768040003010203" + ); + + // with from lock + let event = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: "tokenid1".parse().unwrap(), + from: TokenHolder::Account(AccountAddress([1; 32])), + to: TokenHolder::Account(AccountAddress([2; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + memo: None, + from_lock: Some(LockId::new( + 0x0f0e0d0c0b0a0908, + 0x1122334455667788, + 0x99aabbccddeeff00, + )), + to_lock: None, + }); + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "27000208746f6b656e6964310001010101010101010101010101010101010101010101010101010101010101010002020202020202020202020202020202020202020202020202020202020202028768040f0e0d0c0b0a0908112233445566778899aabbccddeeff00" + ); + + // with to lock + let event = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: "tokenid1".parse().unwrap(), + from: TokenHolder::Account(AccountAddress([1; 32])), + to: TokenHolder::Account(AccountAddress([2; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + memo: None, + from_lock: None, + to_lock: Some(LockId::new( + 0x99aabbccddeeff00, + 0x0f0e0d0c0b0a0908, + 0x1122334455667788, + )), + }); + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "27000408746f6b656e69643100010101010101010101010101010101010101010101010101010101010101010100020202020202020202020202020202020202020202020202020202020202020287680499aabbccddeeff000f0e0d0c0b0a09081122334455667788" + ); + + // with everything + let event = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: "TestTT".parse().unwrap(), + from: TokenHolder::Account(AccountAddress([13; 32])), + to: TokenHolder::Account(AccountAddress([64; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(u64::MAX), + decimals: 255, + }, + memo: Some(Memo::try_from((0x00..=0xff).collect::>()).unwrap()), + from_lock: Some(LockId::new( + 0x0f0e0d0c0b0a0908, + 0x1122334455667788, + 0x99aabbccddeeff00, + )), + to_lock: Some(LockId::new( + 0x7071727374757677, + 0x88898a8b8c8d8e8f, + 0x9f9e9d9c9b9a9998, + )), + }); + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + concat!( + "27", + "000706546573745454", + "000d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d", + "004040404040404040404040404040404040404040404040404040404040404040", + "81ffffffffffffffff7fff", + "0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "0f0e0d0c0b0a0908112233445566778899aabbccddeeff00", + "707172737475767788898a8b8c8d8e8f9f9e9d9c9b9a9998" + ) + ); + } + + #[test] + fn test_token_mint_event_serial() { + let event = BlockItemEvent::TokenMint(TokenMintEvent { + token_id: "tokenid1".parse().unwrap(), + target: TokenHolder::Account(AccountAddress([1; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "28000008746f6b656e696431000101010101010101010101010101010101010101010101010101010101010101876804" + ); + } + + #[test] + fn test_token_burn_event_serial() { + let event = BlockItemEvent::TokenBurn(TokenBurnEvent { + token_id: "tokenid1".parse().unwrap(), + target: TokenHolder::Account(AccountAddress([1; 32])), + amount: TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }, + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "29000008746f6b656e696431000101010101010101010101010101010101010101010101010101010101010101876804" + ); + } + + #[test] + fn test_token_created_event_serial() { + let event = BlockItemEvent::TokenCreated(TokenCreateEvent { + payload: CreatePlt { + token_id: "tokenid1".parse().unwrap(), + token_module: TokenModuleRef::from([5; 32]), + decimals: 2, + initialization_parameters: RawCbor::from(vec![1, 2, 3, 4]), + }, + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "2a08746f6b656e6964310505050505050505050505050505050505050505050505050505050505050505020000000401020304" + ); + } + + #[test] + fn test_lock_created_event_serial() { + let event = BlockItemEvent::LockCreated(LockCreateEvent { + lock_id: LockId::new(12, 10001, 17), + lock_config: RawCbor::from(vec![5, 6, 7, 8]), + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "2b000000000000000c000000000000271100000000000000110000000405060708" + ); + } + + #[test] + fn test_lock_destroyed_event_serial() { + let event = BlockItemEvent::LockDestroyed(LockDestroyEvent { + lock_id: LockId::new(12, 10001, 17), + }); + + let bytes = common::to_bytes(&event); + assert_eq!( + hex::encode(&bytes), + "2c000000000000000c00000000000027110000000000000011" + ); + } +} diff --git a/plt/plt-scheduler-types/src/types/execution.rs b/plt/plt-scheduler-types/src/types/execution.rs new file mode 100644 index 0000000000..788e0ee62a --- /dev/null +++ b/plt/plt-scheduler-types/src/types/execution.rs @@ -0,0 +1,112 @@ +//! Types used specifically related to block item execution. + +use crate::types::events::BlockItemEvent; +use crate::types::reject_reasons::TransactionRejectReason; +use concordium_base::base::Energy; +use concordium_base::common::{Buffer, Put, Serial}; +use concordium_base::protocol_level_tokens::{TokenId, TokenModuleRef}; + +/// Summary of execution a transaction. +#[derive(Debug, Clone)] +pub struct TransactionExecutionSummary { + /// Outcome of executing the transaction. The transaction can either be successful or rejected. + /// If the transaction is rejected, the changes to the block state must be rolled back. + pub outcome: TransactionOutcome, + /// Energy used by the execution. This is always less than the `energy_limit` argument given to `execute_transaction`. + pub energy_used: Energy, +} + +/// Outcome of executing a transaction that was correctly executed (not resulting in the unrecoverable error `TransactionExecutionError`). +/// +/// If the transaction was successful, this is a list of events that represents +/// the changes that were applied to the block state by the transaction. If the transaction was +/// rejected, it is a reject reason, and the changes to the block state must be rolled back. +#[derive(Debug, Clone)] +pub enum TransactionOutcome { + /// The transaction was successfully applied. + Success(Vec), + /// The transaction was rejected. The transaction + /// is included in the block as a rejected transaction. + /// The changes to the block state must be rolled back. + Rejected(TransactionRejectReason), +} + +/// Outcome of executing a chain update that was correctly executed (not resulting in the unrecoverable error `ChainUpdateExecutionError`). +/// +/// If the chain update was successful, this is a list of events that represents +/// the changes that were applied to the block state by the chain update. If the chain update +/// failed, it is a failure kind, and the changes to the block state must be rolled back. +#[derive(Debug, Clone)] +pub enum ChainUpdateOutcome { + /// The chain update was successfully applied. + Success(Vec), + /// The chain update failed and is not included in the block. + /// The changes to the block state must be rolled back. + Failed(FailureKind), +} + +/// Reasons for the execution of a block item to fail. +/// +/// Corresponding Haskell type: `Concordium.Types.Execution.FailureKind` +#[derive(Debug, Clone)] +pub enum FailureKind { + /// A protocol-level token with the given token ID already exists. + DuplicateTokenId(TokenId), + /// The token module encountered an error when initializing the protocol-level token. + TokenInitializeFailure(String), + /// The token module reference is unknown or invalid. + InvalidTokenModuleRef(TokenModuleRef), +} + +impl Serial for FailureKind { + fn serial(&self, out: &mut B) { + match self { + FailureKind::DuplicateTokenId(token_id) => { + out.put(&18u8); + out.put(token_id); + } + FailureKind::TokenInitializeFailure(error) => { + out.put(&19u8); + out.put(error); + } + FailureKind::InvalidTokenModuleRef(module_ref) => { + out.put(&20u8); + out.put(module_ref); + } + } + } +} + +#[cfg(test)] +mod test { + use crate::types::execution::FailureKind; + use concordium_base::common; + use concordium_base::protocol_level_tokens::TokenModuleRef; + + #[test] + fn test_duplicate_token_id_serial() { + let failure_kind = FailureKind::DuplicateTokenId("tokenid1".parse().unwrap()); + + let bytes = common::to_bytes(&failure_kind); + assert_eq!(hex::encode(&bytes), "1208746f6b656e696431"); + } + + #[test] + fn test_token_initialize_failure_serial() { + let failure_kind = FailureKind::TokenInitializeFailure("error1".parse().unwrap()); + + let bytes = common::to_bytes(&failure_kind); + assert_eq!(hex::encode(&bytes), "1300000000000000066572726f7231"); + } + + #[test] + fn test_invalid_token_module_ref_serial() { + let failure_kind = FailureKind::InvalidTokenModuleRef(TokenModuleRef::from([5; 32])); + + let bytes = common::to_bytes(&failure_kind); + assert_eq!( + hex::encode(&bytes), + "140505050505050505050505050505050505050505050505050505050505050505" + ); + } +} diff --git a/plt/plt-scheduler-types/src/types/protocol_version.rs b/plt/plt-scheduler-types/src/types/protocol_version.rs new file mode 100644 index 0000000000..a1d1b125ef --- /dev/null +++ b/plt/plt-scheduler-types/src/types/protocol_version.rs @@ -0,0 +1,37 @@ +use thiserror::Error; + +/// Protocol version relevant for the Rust scheduler. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +pub enum ProtocolVersion { + P9, + P10, + P11, +} + +/// Protocol version was unknown to Rust scheduler. +#[derive(Debug, Error)] +#[error("Protocol version unknown to Rust scheduler: {0}")] +pub struct UnknownProtocolVersion(u64); + +impl TryFrom for ProtocolVersion { + type Error = UnknownProtocolVersion; + + fn try_from(value: u64) -> Result { + match value { + 9 => Ok(ProtocolVersion::P9), + 10 => Ok(ProtocolVersion::P10), + 11 => Ok(ProtocolVersion::P11), + _ => Err(UnknownProtocolVersion(value)), + } + } +} + +impl From for u64 { + fn from(pv: ProtocolVersion) -> Self { + match pv { + ProtocolVersion::P9 => 9, + ProtocolVersion::P10 => 10, + ProtocolVersion::P11 => 11, + } + } +} diff --git a/plt/plt-scheduler-types/src/types/queries.rs b/plt/plt-scheduler-types/src/types/queries.rs new file mode 100644 index 0000000000..7e60c06932 --- /dev/null +++ b/plt/plt-scheduler-types/src/types/queries.rs @@ -0,0 +1,168 @@ +//! Types returned by queries. + +use crate::types::tokens::TokenAmount; +use concordium_base::common::Serialize; +use concordium_base::protocol_level_tokens::{RawCbor, TokenId, TokenModuleRef}; + +/// Token state at the block level +/// +/// Corresponding Haskell type: `Concordium.Types.Queries.Tokens.TokenState` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct TokenState { + /// The reference of the module implementing this token. + pub token_module_ref: TokenModuleRef, + /// Number of decimals in the decimal number representation of amounts. + pub decimals: u8, + /// The total available token supply. + pub total_supply: TokenAmount, + /// Token module specific state, such as token name, feature flags, meta + /// data. + pub module_state: RawCbor, +} + +/// The token state at the block level. +/// +/// Corresponding Haskell type: `Concordium.Types.Queries.Tokens.TokenInfo` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct TokenInfo { + /// The canonical identifier/symbol for the protocol level token. + pub token_id: TokenId, + /// The associated block level state. + pub state: TokenState, +} + +/// State of a protocol level token associated with some account. +/// +/// Corresponding Haskell type: `Concordium.Types.Queries.Tokens.TokenAccountState` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct TokenAccountState { + /// The token balance of the account. + pub balance: TokenAmount, + /// The token-module defined state of the account. + pub module_state: Option, +} + +/// State of a protocol level token associated with some account. +/// +/// Corresponding Haskell type: `Concordium.Types.Queries.Tokens.Token` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct TokenAccountInfo { + /// The canonical identifier/symbol for the protocol level token. + pub token_id: TokenId, + /// The state of the token associated with the account. + pub account_state: TokenAccountState, +} + +/// The token authorizations returned for a `TokenAuthorizationQuery`. +/// +/// Corresponding Haskell type: `Concordium.Types.Queries.Tokens.TokenAuthorzations` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct TokenAuthorizations { + /// The canonical identifier/symbol for the protocol level token. + pub token_id: TokenId, + /// The CBOR encoding of `token-authorizations` found in `concordium-base/cddl/cis-7.cddl`. + pub details: RawCbor, +} + +#[cfg(test)] +mod test { + use concordium_base::{ + common, + protocol_level_tokens::{RawCbor, TokenId, TokenModuleRef}, + }; + + use crate::types::{ + queries::{TokenAccountInfo, TokenAccountState, TokenInfo, TokenState}, + tokens::{RawTokenAmount, TokenAmount}, + }; + + fn module_state_fixture() -> RawCbor { + vec![1, 2, 3].into() + } + + fn token_amount_fixture(decimals: u8) -> TokenAmount { + TokenAmount { + amount: RawTokenAmount::from(100), + decimals, + } + } + + fn token_state_fixture() -> TokenState { + TokenState { + token_module_ref: TokenModuleRef::from([1; 32]), + decimals: 10, + total_supply: token_amount_fixture(10), + module_state: module_state_fixture(), + } + } + + fn token_id_fixture() -> TokenId { + "token" + .to_string() + .try_into() + .expect("token id must be valid") + } + + fn token_account_state_fixture() -> TokenAccountState { + TokenAccountState { + balance: token_amount_fixture(10), + module_state: Some(module_state_fixture()), + } + } + + #[test] + fn test_token_state_serial() { + let token_state = token_state_fixture(); + + let bytes = common::to_bytes(&token_state); + assert_eq!( + hex::encode(&bytes), + "01010101010101010101010101010101010101010101010101010101010101010a640a00000003010203" + ); + + let deserialized = common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_state, deserialized); + } + + #[test] + fn test_token_info_serial() { + let token_info = TokenInfo { + token_id: token_id_fixture(), + state: token_state_fixture(), + }; + + let bytes = common::to_bytes(&token_info); + assert_eq!( + hex::encode(&bytes), + "05746f6b656e01010101010101010101010101010101010101010101010101010101010101010a640a00000003010203" + ); + + let deserialized = common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_info, deserialized); + } + + #[test] + fn test_token_account_state_serial() { + let token_account_state = token_account_state_fixture(); + + let bytes = common::to_bytes(&token_account_state); + assert_eq!(hex::encode(&bytes), "640a0100000003010203"); + + let deserialized = common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_account_state, deserialized); + } + + #[test] + fn test_token_account_info_serial() { + let token_account_info = TokenAccountInfo { + token_id: token_id_fixture(), + account_state: token_account_state_fixture(), + }; + + let bytes = common::to_bytes(&token_account_info); + assert_eq!(hex::encode(&bytes), "05746f6b656e640a0100000003010203"); + + let deserialized = common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_account_info, deserialized); + } +} diff --git a/plt/plt-scheduler-types/src/types/reject_reasons.rs b/plt/plt-scheduler-types/src/types/reject_reasons.rs new file mode 100644 index 0000000000..63d54647f7 --- /dev/null +++ b/plt/plt-scheduler-types/src/types/reject_reasons.rs @@ -0,0 +1,322 @@ +//! Reject reasons for transactions executed by the scheduler. +//! +//! A rejected transaction means that the transaction was included on chain, but +//! failed for some reason. The only effect of a rejected transaction is +//! the charge of energy. + +use concordium_base::common::{Buffer, Put, Serial}; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::{RawCbor, TokenId, TokenModuleCborTypeDiscriminator}; + +/// A reason for why a transaction was rejected. +/// +/// Rejected means included in a +/// block, but the desired action was not achieved. The only effect of a +/// rejected transaction is paying for the energy used. +/// +/// Corresponding Haskell type: `Concordium.Types.Execution.RejectReason` +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum TransactionRejectReason { + /// Account does not exist. + InvalidAccountReference(AccountAddress), + /// The transaction payload could not be fully deserialized. + SerializationFailure, + /// We ran of out energy to process this transaction. + OutOfEnergy, + /// The provided identifier does not match a token currently on chain. + NonExistentTokenId(TokenId), + /// The token module rejected the transaction. + TokenUpdateTransactionFailed(EncodedTokenModuleRejectReason), + /// Lock ID does not exist. + NonExistentLockId(LockId), + /// The lock is expired. + LockExpired(LockId), + /// The account is not authorized to fund the lock. + LockFundNotAuthorized(LockId, AccountAddress), + /// The account is not authorized to send funds controlled by the lock. + LockSendNotAuthorized(LockId, AccountAddress), + /// The account is not authorized to return funds controlled by the lock. + LockReturnNotAuthorized(LockId, AccountAddress), + /// The account is not authorized to cancel the lock. + LockCancelNotAuthorized(LockId, AccountAddress), + /// The lock does not allow funding with the particular token. + LockTokenNotPermitted(LockId, TokenId), + /// The recipient is not permitted to receive funds controlled by the lock. + LockRecipientNotPermitted(LockId, AccountAddress), + /// The requested expiry exceeds the maximum permitted lock duration. + LockDurationTooLong(LockId), +} + +impl Serial for TransactionRejectReason { + fn serial(&self, out: &mut B) { + match self { + TransactionRejectReason::InvalidAccountReference(address) => { + out.put(&2u8); + out.put(address); + } + TransactionRejectReason::SerializationFailure => { + out.put(&9u8); + } + TransactionRejectReason::OutOfEnergy => { + out.put(&10u8); + } + TransactionRejectReason::NonExistentTokenId(token_id) => { + out.put(&55u8); + out.put(&token_id); + } + TransactionRejectReason::TokenUpdateTransactionFailed(reject_reason) => { + out.put(&56u8); + out.put(&reject_reason); + } + TransactionRejectReason::NonExistentLockId(lock_id) => { + out.put(&57u8); + out.put(&lock_id); + } + TransactionRejectReason::LockExpired(lock_id) => { + out.put(&58u8); + out.put(&lock_id); + } + TransactionRejectReason::LockFundNotAuthorized(lock_id, addr) => { + out.put(&59u8); + out.put(&lock_id); + out.put(&addr); + } + TransactionRejectReason::LockSendNotAuthorized(lock_id, addr) => { + out.put(&60u8); + out.put(&lock_id); + out.put(&addr); + } + TransactionRejectReason::LockReturnNotAuthorized(lock_id, addr) => { + out.put(&61u8); + out.put(&lock_id); + out.put(&addr); + } + TransactionRejectReason::LockCancelNotAuthorized(lock_id, addr) => { + out.put(&62u8); + out.put(&lock_id); + out.put(&addr); + } + TransactionRejectReason::LockTokenNotPermitted(lock_id, token_id) => { + out.put(&63u8); + out.put(&lock_id); + out.put(&token_id); + } + TransactionRejectReason::LockRecipientNotPermitted(lock_id, addr) => { + out.put(&64u8); + out.put(&lock_id); + out.put(&addr); + } + TransactionRejectReason::LockDurationTooLong(lock_id) => { + out.put(&65u8); + out.put(&lock_id); + } + } + } +} + +/// Details provided by the token module in the event of rejecting a +/// transaction. +/// +/// Corresponding Haskell type: `Concordium.Types.TokenModuleRejectReason` +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serial)] +pub struct EncodedTokenModuleRejectReason { + /// The canonical token id. + pub token_id: TokenId, + /// The type of the reject reason. + pub reason_type: TokenModuleCborTypeDiscriminator, + /// (Optional) CBOR-encoded details. + pub details: Option, +} + +#[cfg(test)] +mod test { + use crate::types::reject_reasons::{EncodedTokenModuleRejectReason, TransactionRejectReason}; + use concordium_base::common; + use concordium_base::contracts_common::AccountAddress; + use concordium_base::protocol_level_locks::LockId; + use concordium_base::protocol_level_tokens::RawCbor; + + const ADDRESS: AccountAddress = AccountAddress([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]); + + #[test] + fn test_invalid_account_reference_reject_reason_serial() { + let reject_reason = TransactionRejectReason::InvalidAccountReference(ADDRESS); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "02000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_serialization_failure_reject_reason_serial() { + let reject_reason = TransactionRejectReason::SerializationFailure; + + let bytes = common::to_bytes(&reject_reason); + assert_eq!(hex::encode(&bytes), "09") + } + + #[test] + fn test_out_of_energy_reject_reason_serial() { + let reject_reason = TransactionRejectReason::OutOfEnergy; + + let bytes = common::to_bytes(&reject_reason); + assert_eq!(hex::encode(&bytes), "0a"); + } + + #[test] + fn test_non_existent_token_id_reject_reason_serial() { + let reject_reason = TransactionRejectReason::NonExistentTokenId("token1".parse().unwrap()); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!(hex::encode(&bytes), "3706746f6b656e31"); + } + + #[test] + fn test_token_update_transaction_failed_reject_reason_serial() { + // without details + let reject_reason = + TransactionRejectReason::TokenUpdateTransactionFailed(EncodedTokenModuleRejectReason { + token_id: "token1".parse().unwrap(), + reason_type: "reject_reason_type1".parse().unwrap(), + details: None, + }); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3806746f6b656e311372656a6563745f726561736f6e5f747970653100" + ); + + // with details + let reject_reason = + TransactionRejectReason::TokenUpdateTransactionFailed(EncodedTokenModuleRejectReason { + token_id: "token1".parse().unwrap(), + reason_type: "reject_reason_type1".parse().unwrap(), + details: Some(RawCbor::from(vec![1, 2, 3])), + }); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3806746f6b656e311372656a6563745f726561736f6e5f74797065310100000003010203" + ); + } + + #[test] + fn test_non_existent_lock_id_reject_reason_serial() { + let reject_reason = + TransactionRejectReason::NonExistentLockId(LockId::new(0xfedcba, 0x1234, 5)); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "390000000000fedcba00000000000012340000000000000005" + ); + } + + #[test] + fn test_lock_expired_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockExpired(LockId::new(0xfedcba, 0x1234, 5)); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3a0000000000fedcba00000000000012340000000000000005" + ); + } + + #[test] + fn test_lock_fund_not_authorized_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockFundNotAuthorized( + LockId::new(0xfedcba, 0x1234, 5), + ADDRESS, + ); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3b0000000000fedcba00000000000012340000000000000005000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_lock_send_not_authorized_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockSendNotAuthorized( + LockId::new(0xfedcba, 0x1234, 5), + ADDRESS, + ); + + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3c0000000000fedcba00000000000012340000000000000005000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_lock_return_not_authorized_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockReturnNotAuthorized( + LockId::new(0xfedcba, 0x1234, 5), + ADDRESS, + ); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3d0000000000fedcba00000000000012340000000000000005000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_lock_cancel_not_authorized_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockCancelNotAuthorized( + LockId::new(0xfedcba, 0x1234, 5), + ADDRESS, + ); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3e0000000000fedcba00000000000012340000000000000005000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_lock_token_not_permitted_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockTokenNotPermitted( + LockId::new(0xfedcba, 0x1234, 5), + "token1".parse().unwrap(), + ); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "3f0000000000fedcba0000000000001234000000000000000506746f6b656e31" + ); + } + + #[test] + fn test_lock_recipient_not_permitted_reject_reason_serial() { + let reject_reason = TransactionRejectReason::LockRecipientNotPermitted( + LockId::new(0xfedcba, 0x1234, 5), + ADDRESS, + ); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "400000000000fedcba00000000000012340000000000000005000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ); + } + + #[test] + fn test_lock_duration_too_long_reject_reason_serial() { + let reject_reason = + TransactionRejectReason::LockDurationTooLong(LockId::new(0xfedcba, 0x1234, 5)); + let bytes = common::to_bytes(&reject_reason); + assert_eq!( + hex::encode(&bytes), + "410000000000fedcba00000000000012340000000000000005" + ); + } +} diff --git a/plt/plt-scheduler-types/src/types/tokens.rs b/plt/plt-scheduler-types/src/types/tokens.rs new file mode 100644 index 0000000000..0611ac06dd --- /dev/null +++ b/plt/plt-scheduler-types/src/types/tokens.rs @@ -0,0 +1,352 @@ +use concordium_base::common::__serialize_private::anyhow::bail; +use concordium_base::common::{ + Buffer, Deserial, Get, ParseResult, Put, ReadBytesExt, Serial, Serialize, +}; +use concordium_base::contracts_common::AccountAddress; + +/// 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. +/// +/// Corresponding Haskell type: `Concordium.Types.Tokens.TokenRawAmount` +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] +pub struct RawTokenAmount(u64); + +impl RawTokenAmount { + /// Maximum representable raw token amount. + pub const MAX: Self = Self(u64::MAX); + + /// Checked addition of raw token amounts. Returns `None` if the result would overflow. + pub fn checked_add(self, other: RawTokenAmount) -> Option { + self.0.checked_add(other.0).map(RawTokenAmount) + } + + /// Checked subtraction of raw token amounts. Returns `None` if the result would overflow. + pub fn checked_sub(self, other: RawTokenAmount) -> Option { + self.0.checked_sub(other.0).map(RawTokenAmount) + } + + /// The [`RawTokenAmount`] value. + pub fn value(self) -> u64 { + self.0 + } +} + +impl From for RawTokenAmount { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(value: RawTokenAmount) -> Self { + value.0 + } +} + +/// Serialization of 'TokenRawAmount' is as a variable length quantity (VLQ). +/// +/// The VLQ encoding represents a value in big-endian base 128. Each byte of the encoding uses +/// the high-order bit to indicate if further bytes follow (when set). The remaining bits represent +/// the positional value in base 128. See +impl Serial for RawTokenAmount { + fn serial(&self, out: &mut B) { + // Maximum number of bytes a u64 can be serialized to. + const MAX_BYTES: usize = 10; + // Use buffer for the serialized bytes since we produce bytes + // in the opposite order of which we want to write them + // (we produce the least significant first). + let mut buffer = [0; MAX_BYTES]; + let mut buffer_index = MAX_BYTES - 1; + + let mut val = self.0; + + // The least significant byte. This byte is always there + // and never has the continuation byte set, since it is the last. + let byte = val as u8 & 0x7fu8; + buffer[buffer_index] = byte; + val >>= 7; + + // Following bytes in order of more significant. Continuation + // bit (0x80) is always set, since there is always a following byte. + while val != 0 { + let byte = 0x80u8 | (val as u8 & 0x7fu8); + buffer_index -= 1; + buffer[buffer_index] = byte; + val >>= 7; + } + + // Write bytes in correct order. + for byte in &buffer[buffer_index..MAX_BYTES] { + out.put(&byte); + } + } +} + +/// Deserialization of 'TokenRawAmount' is as a variable length quantity (VLQ). We disallow +/// 0-padding to enforce canonical serialization. +/// +/// The VLQ encoding represents a value in big-endian base 128. Each byte of the encoding uses +/// the high-order bit to indicate if further bytes follow (when set). The remaining bits represent +/// the positional value in base 128. See +impl Deserial for RawTokenAmount { + fn deserial(source: &mut R) -> ParseResult { + // Decode first byte and use 7 bits from it. + let mut byte: u8 = source.get()?; + let mut value = 0x7f & byte as u64; + + // Check for 0-padding. + if 0x80 & byte != 0 && value == 0 { + bail!("Token amount padded with zeros"); + } + + // Decode additional byte if 8'th bit is set. + while 0x80 & byte != 0 { + // Check if shifting would overflow the value. + if value & (0x7f << (64 - 7)) != 0 { + bail!("Token amount not representable as u64"); + } + + // Decode additional byte and use 7 bits from it as least significant bytes in value. + byte = source.get()?; + value <<= 7; + value |= 0x7f & byte as u64; + } + + Ok(Self(value)) + } +} + +/// Protocol level token (PLT) amount representation. The numerical amount +/// represented is `value * 10^(-decimals)`. +/// The number of decimals in the token amount should always match the number of +/// decimals for the token it represents an amount for. +/// +/// Corresponding Haskell type: `Concordium.Types.Tokens.TokenAmount` +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize)] +pub struct TokenAmount { + /// The amount of tokens as an unscaled integer value. + pub amount: RawTokenAmount, + /// The number of decimals in the token amount. + pub decimals: u8, +} + +impl TokenAmount { + pub fn from_raw(amount: u64, decimals: u8) -> Self { + Self { + amount: RawTokenAmount::from(amount), + decimals, + } + } +} + +/// Token holder. +/// +/// Corresponding Haskell type: `Concordium.Types.TokenHolder` +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize)] +pub enum TokenHolder { + Account(AccountAddress), +} + +#[cfg(test)] +mod test { + use crate::types::tokens::{RawTokenAmount, TokenAmount, TokenHolder}; + use concordium_base::common; + use concordium_base::common::ParseResult; + use concordium_base::contracts_common::AccountAddress; + use proptest::prelude::ProptestConfig; + use proptest::{prop_assert, prop_assert_eq, proptest}; + + /// Test special cases for successful raw token amount serialization/deserialization + /// and for deserialization failures. This test is supplemented + /// with the property test [`prop_test_raw_token_amount_serial`]. + #[test] + fn test_raw_token_amount_serial() { + let token_amount = RawTokenAmount::from(0); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "00"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "01"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(2); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "02"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(127); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "7f"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "8100"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(129); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "8101"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128 - 1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "ff7f"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "818000"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128 + 1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "818001"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128 * 128 - 1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "ffff7f"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128 * 128); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "81808000"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(128 * 128 * 128 + 1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "81808001"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(u64::MAX - 1); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "81ffffffffffffffff7e"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + let token_amount = RawTokenAmount::from(u64::MAX); + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "81ffffffffffffffff7f"); + let token_amount_deserialized: RawTokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + + // Test out of range value + let bytes = hex::decode("82808080808080808000").unwrap(); + let err = common::from_bytes_complete::(&mut bytes.as_slice()) + .expect_err("deserialize"); + assert!( + err.to_string() + .contains("Token amount not representable as u64"), + "err: {}", + err + ); + + // Test out of range value + let bytes = hex::decode("84808080808080808000").unwrap(); + let err = common::from_bytes_complete::(&mut bytes.as_slice()) + .expect_err("deserialize"); + assert!( + err.to_string() + .contains("Token amount not representable as u64"), + "err: {}", + err + ); + + // Test value padded with 0 + let bytes = hex::decode("8001").unwrap(); + let err = common::from_bytes_complete::(&mut bytes.as_slice()) + .expect_err("deserialize"); + assert!( + err.to_string().contains("Token amount padded with zeros"), + "err: {}", + err + ); + + // Test value padded with 0 + let bytes = hex::decode("808101").unwrap(); + let err = common::from_bytes_complete::(&mut bytes.as_slice()) + .expect_err("deserialize"); + assert!( + err.to_string().contains("Token amount padded with zeros"), + "err: {}", + err + ); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(10000))] + + /// Property test of raw token amount serialization/deserialization. Special cases + /// are tested in the test [`test_raw_token_amount_serial`]. + #[test] + fn prop_test_raw_token_amount_serial(value: u64) { + let token_amount = RawTokenAmount::from(value); + let bytes = common::to_bytes(&token_amount); + let token_amount_deserialized_result: ParseResult = + common::from_bytes_complete(bytes.as_slice()); + prop_assert!(token_amount_deserialized_result.is_ok()); + let token_amount_deserialized = token_amount_deserialized_result.unwrap(); + prop_assert_eq!(token_amount_deserialized, token_amount); + } + } + + #[test] + fn test_token_amount_serial() { + let token_amount = TokenAmount { + amount: RawTokenAmount::from(1000), + decimals: 4, + }; + + let bytes = common::to_bytes(&token_amount); + assert_eq!(hex::encode(&bytes), "876804"); + + let token_amount_deserialized: TokenAmount = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_amount_deserialized, token_amount); + } + + #[test] + fn test_token_holder() { + let token_holder = TokenHolder::Account(AccountAddress([5; 32])); + + let bytes = common::to_bytes(&token_holder); + assert_eq!( + hex::encode(&bytes), + "000505050505050505050505050505050505050505050505050505050505050505" + ); + + let token_holder_deserialized: TokenHolder = + common::from_bytes_complete(bytes.as_slice()).unwrap(); + assert_eq!(token_holder_deserialized, token_holder); + } +} diff --git a/plt/plt-scheduler/Cargo.toml b/plt/plt-scheduler/Cargo.toml new file mode 100644 index 0000000000..79c520af47 --- /dev/null +++ b/plt/plt-scheduler/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "plt-scheduler" +version = "0.1.0" +edition = "2024" + +[features] +default = ["ffi"] +ffi = ["dep:libc"] + +[dependencies] +# Workspace dependencies +plt-block-state.workspace = true +plt-scheduler-types.workspace = true + +# Concordium dependencies +concordium_base.workspace = true + +# Third party dependencies +thiserror.workspace = true +libc = { workspace = true, optional = true } + +[dev-dependencies] +assert_matches.workspace = true +hex.workspace = true diff --git a/plt/plt-scheduler/src/block_state_polymorph.rs b/plt/plt-scheduler/src/block_state_polymorph.rs new file mode 100644 index 0000000000..b818fdc370 --- /dev/null +++ b/plt/plt-scheduler/src/block_state_polymorph.rs @@ -0,0 +1,3 @@ +//! Traits implemented for block state types with the purpose of making the scheduler code generic. + +pub mod token; diff --git a/plt/plt-scheduler/src/block_state_polymorph/token.rs b/plt/plt-scheduler/src/block_state_polymorph/token.rs new file mode 100644 index 0000000000..7eb22cd3fb --- /dev/null +++ b/plt/plt-scheduler/src/block_state_polymorph/token.rs @@ -0,0 +1,84 @@ +use plt_block_state::entity::protocol_level_tokens::p9::{TokenP9, TokenP9Base}; +use plt_block_state::entity::protocol_level_tokens::p11::TokenP11; + +/// Token on any protocol version. +pub enum TokenPX { + TokenP9(TokenP9), + TokenP11(TokenP11), +} + +impl TokenPX { + /// Access the token as a base P9 token. + pub fn token_p9_base(&self) -> &TokenP9Base { + match self { + Self::TokenP9(token) => &token.token_p9_base, + Self::TokenP11(token) => &token.token_p9_base, + } + } + + /// Mutate the token as a base P9 token. + pub fn token_p9_base_mut(&mut self) -> &mut TokenP9Base { + match self { + Self::TokenP9(token) => &mut token.token_p9_base, + Self::TokenP11(token) => &mut token.token_p9_base, + } + } +} + +/// Access to token on any protocol version via a unique/mutable reference. +#[derive(Debug)] +pub enum TokenPXRefMut<'a> { + TokenP9(&'a mut TokenP9), + TokenP11(&'a mut TokenP11), +} + +impl<'a> TokenPXRefMut<'a> { + /// Access the token as a base P9 token. + pub fn token_p9_base(&self) -> &TokenP9Base { + match self { + Self::TokenP9(token) => &token.token_p9_base, + Self::TokenP11(token) => &token.token_p9_base, + } + } + + /// Mutate the token as a base P9 token. + pub fn token_p9_base_mut(&mut self) -> &mut TokenP9Base { + match self { + Self::TokenP9(token) => &mut token.token_p9_base, + Self::TokenP11(token) => &mut token.token_p9_base, + } + } + + /// Get non-mutable reference to the token. + pub fn as_ref(&self) -> TokenPXRef<'_> { + match self { + Self::TokenP9(token) => TokenPXRef::TokenP9(token), + Self::TokenP11(token) => TokenPXRef::TokenP11(token), + } + } + + /// Reborrow the mutable reference to the token. + pub fn as_mut(&mut self) -> TokenPXRefMut<'_> { + match self { + Self::TokenP9(token) => TokenPXRefMut::TokenP9(token), + Self::TokenP11(token) => TokenPXRefMut::TokenP11(token), + } + } +} + +/// Access to a token on any protocol version via a shared reference. +#[derive(Debug, Clone, Copy)] +pub enum TokenPXRef<'a> { + TokenP9(&'a TokenP9), + TokenP11(&'a TokenP11), +} + +impl<'a> TokenPXRef<'a> { + /// Access the token as a base P9 token. + pub fn token_p9_base(&self) -> &'a TokenP9Base { + match self { + Self::TokenP9(token) => &token.token_p9_base, + Self::TokenP11(token) => &token.token_p9_base, + } + } +} diff --git a/plt/plt-scheduler/src/failure.rs b/plt/plt-scheduler/src/failure.rs new file mode 100644 index 0000000000..5c0b56680a --- /dev/null +++ b/plt/plt-scheduler/src/failure.rs @@ -0,0 +1,69 @@ +use plt_block_state::entity::block_state::{LockNotFoundByIdError, TokenNotFoundByIdError}; +use plt_block_state::external::AccountNotFoundByAddressError; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; + +/// [`BlockStateFailure`] and [`T`] flattened into one error +/// for convenience. +#[derive(Debug, thiserror::Error)] +pub enum WithBlockStateFailure { + /// Higher protocol level error + #[error("{0}")] + Error(T), + /// An unrecoverable error occurred in block state when executing the transaction. + #[error("Block state failure: {0}")] + BlockStateFailure(#[from] BlockStateFailure), +} + +/// Marker trait that allows an error to be used in [`WithBlockStateFailure`] (acts as a +/// "negative" bound in the `From` implementation to avoid conflict with `From`). +pub trait HigherLevelProtocolError {} + +impl, F> From for WithBlockStateFailure { + fn from(error: E) -> Self { + Self::Error(error.into()) + } +} + +pub type ResultWithBlockStateFailure = Result>; + +impl HigherLevelProtocolError for TransactionRejectReason {} +impl HigherLevelProtocolError for LockNotFoundByIdError {} +impl HigherLevelProtocolError for AccountNotFoundByAddressError {} +impl HigherLevelProtocolError for TokenNotFoundByIdError {} + +/// Extension trait for [`ResultWithBlockStateFailure`] +pub trait ResultWithBlockStateFailureExt { + /// Map [`ResultWithBlockStateFailure`] to two nested results, with [`BlockStateFailure`] + /// as the error type in the outer, and the higher level protocol + /// error as the error type in the inner. + fn nest(self) -> BlockStateResult>; + + /// Map the inner higher level protocol error in [`ResultWithBlockStateFailure`] using `op`. + fn map_nested_err(self, op: O) -> ResultWithBlockStateFailure + where + O: FnOnce(E) -> F; +} + +impl ResultWithBlockStateFailureExt for ResultWithBlockStateFailure { + fn nest(self) -> BlockStateResult> { + match self { + Ok(t) => Ok(Ok(t)), + Err(WithBlockStateFailure::BlockStateFailure(failure)) => Err(failure), + Err(WithBlockStateFailure::Error(err)) => Ok(Err(err)), + } + } + + fn map_nested_err(self, op: O) -> ResultWithBlockStateFailure + where + O: FnOnce(E) -> F, + { + match self { + Ok(t) => Ok(t), + Err(WithBlockStateFailure::BlockStateFailure(failure)) => { + Err(WithBlockStateFailure::BlockStateFailure(failure)) + } + Err(WithBlockStateFailure::Error(err)) => Err(WithBlockStateFailure::Error(op(err))), + } + } +} diff --git a/plt/plt-scheduler/src/ffi.rs b/plt/plt-scheduler/src/ffi.rs new file mode 100644 index 0000000000..2579f2b139 --- /dev/null +++ b/plt/plt-scheduler/src/ffi.rs @@ -0,0 +1,7 @@ +//! This module provides a C ABI for the Rust PLT scheduler library. +//! +//! It is only available if the `ffi` feature is enabled. + +mod queries; +mod scheduler; +mod status; diff --git a/plt/plt-scheduler/src/ffi/queries.rs b/plt/plt-scheduler/src/ffi/queries.rs new file mode 100644 index 0000000000..bf8bbd5dde --- /dev/null +++ b/plt/plt-scheduler/src/ffi/queries.rs @@ -0,0 +1,666 @@ +//! This module provides a C ABI for the Rust PLT scheduler functions. +//! +//! It is only available if the `ffi` feature is enabled. + +use crate::failure::ResultWithBlockStateFailureExt; +use crate::ffi::status; +use crate::{protocol_level_locks, protocol_level_tokens}; +use concordium_base::base::AccountIndex; +use concordium_base::common; +use concordium_base::protocol_level_locks::LockId; +use libc::size_t; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::block_state::p10::BlockStateP10; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::block_state::{LockNotFoundByIdError, TokenNotFoundByIdError}; +use plt_block_state::entity::{EntityContext, EntityContextTypesWitness}; +use plt_block_state::ffi::blob_store_callbacks::LoadCallback; +use plt_block_state::ffi::block_state_callbacks::{ + ExternalBlockStateQueryCallbacks, GetAccountIndexByAddressCallback, + GetCanonicalAddressByAccountIndexCallback, GetTokenAccountStatesCallback, + ReadTokenAccountBalanceCallback, +}; +use plt_block_state::ffi::memory; +use plt_block_state::persistent::block_state::PersistentBlockState; + +/// Context with write access to external block state (will panic if accessed). +pub type FfiQueryEntityContext = + EntityContext>; + +/// C-binding for calling [`queries::query_plt_list`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `return_data_out` Location for writing pointer to array containing return data, which is serialized tokens ids. +/// If the return value is [`status::FfiStatusCode::Success`], the data is a list of token ids. +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`crate::block_state::BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_query_plt_list( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + let token_ids_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_plt_list(&context, &block_state) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_plt_list(&context, &block_state) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p11::query_plt_list(&context, &block_state) + } + }; + match token_ids_res { + Ok(token_ids) => { + let return_data = common::to_bytes(&token_ids); + (status::FfiStatusCode::Success, return_data) + } + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`queries::query_token_info`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Failed`]: Token does not exist +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `token_id` Shared pointer to token id UTF-8 bytes. +/// - `token_id_len` Byte length of token id UTF-8 bytes. +/// - `return_data_out` Location for writing pointer to array containing return data, which is the serialized token info. +/// If the return value is [`status::FfiStatusCode::Success`], the data is the token info. +/// If the return value is [`status::FfiStatusCode::Failed`], the data is empty (zero bytes). +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`crate::block_state::BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `token_id` must be non-null and valid for reads for `token_id_len` many bytes. +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_query_token_info( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + token_id: *const u8, + token_id_len: size_t, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + let token_id_bytes = unsafe { std::slice::from_raw_parts(token_id, token_id_len) }; + let token_id = String::from_utf8(token_id_bytes.to_vec()) + .expect("Bytes for the Token ID is not a valid UTF-8 encoding") + .try_into() + .expect("Invalid Token ID provided"); + let token_info_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_info(&context, &block_state, &token_id) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_info(&context, &block_state, &token_id) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p11::query_token_info(&context, &block_state, &token_id) + } + }; + match token_info_res.nest() { + Ok(Ok(token_info)) => ( + status::FfiStatusCode::Success, + common::to_bytes(&token_info), + ), + Ok(Err(TokenNotFoundByIdError(_))) => (status::FfiStatusCode::Failed, Vec::new()), + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`queries::query_token_authorizations`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Failed`]: Token does not exist +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `token_id` Shared pointer to token id UTF-8 bytes. +/// - `token_id_len` Byte length of token id UTF-8 bytes. +/// - `return_data_out` Location for writing pointer to array containing return data, which is the serialized token info. +/// If the return value is [`status::FfiStatusCode::Success`], the data is the token info. +/// If the return value is [`status::FfiStatusCode::Failed`], the data is empty (zero bytes). +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`crate::block_state::BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `token_id` must be non-null and valid for reads for `token_id_len` many bytes. +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_query_token_authorizations( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + token_id: *const u8, + token_id_len: size_t, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + let token_id_bytes = unsafe { std::slice::from_raw_parts(token_id, token_id_len) }; + let token_id = String::from_utf8(token_id_bytes.to_vec()) + .expect("Bytes for the Token ID is not a valid UTF-8 encoding") + .try_into() + .expect("Invalid Token ID provided"); + let token_auths_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_authorizations( + &context, + &block_state, + &token_id, + ) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_authorizations( + &context, + &block_state, + &token_id, + ) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p11::query_token_authorizations( + &context, + &block_state, + &token_id, + ) + } + }; + match token_auths_res.nest() { + Ok(Ok(token_auths)) => ( + status::FfiStatusCode::Success, + common::to_bytes(&token_auths), + ), + Ok(Err(TokenNotFoundByIdError(_))) => (status::FfiStatusCode::Failed, Vec::new()), + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`queries::query_token_account_infos`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `account_index` Index of the account to find token account infos for. The account must exist. +/// - `return_data_out` Location for writing pointer to array containing return data, which is the serialized token account infos. +/// If the return value is [`status::FfiStatusCode::Success`], the data is the serialized list of token account infos. +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`crate::block_state::PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_query_token_account_infos( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + account_index: u64, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + let token_account_infos_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_account_infos( + &context, + &block_state, + Account::from_existing_account(AccountIndex::from(account_index)), + ) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p9::query_token_account_infos( + &context, + &block_state, + Account::from_existing_account(AccountIndex::from(account_index)), + ) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_tokens::p11::query_token_account_infos( + &context, + &block_state, + Account::from_existing_account(AccountIndex::from(account_index)), + ) + } + }; + match token_account_infos_res { + Ok(token_account_infos) => { + let return_data = common::to_bytes(&token_account_infos); + (status::FfiStatusCode::Success, return_data) + } + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`queries::query_lock_list`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `return_data_out` Location for writing pointer to array containing return data, which is the +/// serialized list of lock ids. The pointer written is to a uniquely owned array. The caller must +/// free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written +/// to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be valid function pointers to functions with a signature matching +/// the signature of the Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to a well-formed [`crate::block_state::PersistentBlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through +/// interior mutability). +/// - Argument `return_data_out` must be a non-null and valid pointer for writing. +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_query_lock_list( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + let lock_ids_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_locks::p9::query_lock_list(&context, &block_state) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_locks::p9::query_lock_list(&context, &block_state) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_locks::p11::query_lock_list(&context, &block_state) + } + }; + + match lock_ids_res { + Ok(lock_ids) => (status::FfiStatusCode::Success, common::to_bytes(&lock_ids)), + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`queries::query_lock_info`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Query succeeded +/// - [`status::FfiStatusCode::Failed`]: Lock does not exist +/// - [`status::FfiStatusCode::Panic`]: Execution of the query resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use for queries. +/// - `lock_id` Pointer to 24 bytes containing the [`LockId`] (three big-endian `u64` fields: +/// `account_index`, `sequence_number`, `creation_order`). +/// - `return_data_out` Location for writing pointer to array containing return data, which is the +/// raw CBOR-encoded `lock-info` payload. The pointer written is to a uniquely owned array; the +/// caller must free the written array using `free_array_len_2` when it is no longer used. +/// If the return value is [`status::FfiStatusCode::Failed`], the data is empty (zero bytes). +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written +/// to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be valid function pointers to functions with a signature matching +/// the signature of the Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to a well-formed [`crate::block_state::BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through +/// interior mutability). +/// - Argument `lock_id` must be non-null and valid for reads of exactly 24 bytes. +/// - Argument `return_data_out` must be a non-null and valid pointer for writing. +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_query_lock_info( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + lock_id: *const [u8; 24], + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!(!lock_id.is_null(), "lock_id is a null pointer."); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }; + let context = FfiQueryEntityContext { + external, + store: load_callback, + }; + // The Haskell side serializes a `LockId` as three big-endian `u64`s, exactly 24 bytes. + let lock_id_bytes = unsafe { lock_id.as_ref().expect("lock_id is a null pointer") }; + let lock_id: LockId = common::from_bytes_complete(lock_id_bytes) + .expect("Bytes for the LockId could not be deserialized"); + let lock_info_res = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + protocol_level_locks::p9::query_lock_info(&context, &block_state, &lock_id) + } + PersistentBlockState::P10(persistent) => { + let block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + protocol_level_locks::p9::query_lock_info(&context, &block_state, &lock_id) + } + PersistentBlockState::P11(persistent) => { + let block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + protocol_level_locks::p11::query_lock_info(&context, &block_state, &lock_id) + } + }; + match lock_info_res.nest() { + Ok(Ok(cbor_bytes)) => (status::FfiStatusCode::Success, cbor_bytes.into()), + Ok(Err(LockNotFoundByIdError(_))) => (status::FfiStatusCode::Failed, Vec::new()), + Err(err) => (status::FfiStatusCode::Panic, err.to_string().into_bytes()), + } + }); + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} diff --git a/plt/plt-scheduler/src/ffi/scheduler.rs b/plt/plt-scheduler/src/ffi/scheduler.rs new file mode 100644 index 0000000000..d870b1ff7a --- /dev/null +++ b/plt/plt-scheduler/src/ffi/scheduler.rs @@ -0,0 +1,552 @@ +//! This module provides a C ABI for the Rust PLT scheduler query functions. +//! +//! It is only available if the `ffi` feature is enabled. + +use crate::ffi::status; +use crate::scheduler; +use crate::transaction_execution::TransactionContext; +use concordium_base::base::{AccountIndex, Energy, Nonce}; +use concordium_base::contracts_common::{AccountAddress, Timestamp}; +use concordium_base::transactions::Payload; +use concordium_base::updates::UpdatePayload; +use concordium_base::{common, contracts_common}; +use libc::size_t; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::block_state::p10::BlockStateP10; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::{EntityContext, EntityContextTypesWitness}; +use plt_block_state::ffi::blob_store_callbacks::LoadCallback; +use plt_block_state::ffi::block_state_callbacks::{ + ExternalBlockStateOperationCallbacks, ExternalBlockStateQueryCallbacks, + GetAccountIndexByAddressCallback, GetCanonicalAddressByAccountIndexCallback, + GetTokenAccountStatesCallback, IncrementPltUpdateSequenceNumberCallback, + ReadTokenAccountBalanceCallback, TouchTokenAccountCallback, UpdateTokenAccountBalanceCallback, +}; +use plt_block_state::ffi::memory; +use plt_block_state::persistent::block_state::PersistentBlockState; +use plt_block_state::persistent::chain_parameters::PersistentChainParameters; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, TransactionOutcome}; +use std::marker::PhantomData; + +/// Context with full external block state. +pub type FfiSchedulerEntityContext = + EntityContext>; + +/// C-binding for calling [`scheduler::execute_transaction`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Transaction execution succeeded and transaction was applied to block state. +/// - [`status::FfiStatusCode::Failed`]: Transaction was rejected with a reject reason. Block state changes applied +/// via callbacks must be rolled back. +/// - [`status::FfiStatusCode::Panic`]: Execution of the transaction resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `update_token_account_balance_callback` External function to call updating the token balance of an account. +/// - `touch_token_account_callback` External function to call to touch token account state. +/// - `increment_plt_update_sequence_number_callback` External function for incrementing the PLT update instruction sequence number. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use as input state to execution. +/// - `external_chain_parameters` Read-only pointer to P11 external chain parameters. It must be +/// null for P9/P10 and non-null for P11. +/// - `payload` Shared pointer to transaction payload bytes. +/// - `payload_len` Byte length of transaction payload. +/// - `sender_account_index` The account index of the account which signed as the sender of the transaction. +/// - `sender_account_address` The account address of the account which signed as the sender of the transaction. +/// - `transaction_sequence_number` The account sequence number (nonce) of the transaction to execute. +/// - `block_timestamp` Timestamp of the block in which the transaction is executed. +/// - `remaining_energy` The remaining energy at the start of the execution. +/// - `block_state_out` Location for writing the pointer of the updated block state. +/// The block state is only written if return value is [`status::FfiStatusCode::Success`]. +/// The pointer written is to a uniquely owned instance. +/// The caller must free the written block state using `ffi_free_plt_block_state` when it is no longer used. +/// - `used_energy_out` Location for writing the energy used by the execution. +/// - `return_data_out` Location for writing pointer to array containing return data, which is either serialized events or reject reason. +/// If the return value is [`status::FfiStatusCode::Success`], the data is a list of block item events. If the return value +/// is [`status::FfiStatusCode::Failed`], it is a transaction reject reason. +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `external_chain_parameters` must be null for P9/P10. For P11 it must be a non-null +/// pointer to well-formed [`PersistentChainParameters`], valid for the duration of this call. +/// - Argument `payload` must be non-null and valid for reads for `payload_len` many bytes. +/// - Argument `sender_account_address` must be non-null and valid for reads for 32 bytes. +/// - Argument `block_state_out` must be a non-null and valid pointer for writing +/// - Argument `used_energy_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_execute_transaction( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + update_token_account_balance_callback: UpdateTokenAccountBalanceCallback, + touch_token_account_callback: TouchTokenAccountCallback, + increment_plt_update_sequence_number_callback: IncrementPltUpdateSequenceNumberCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + external_chain_parameters: *const PersistentChainParameters, + payload: *const u8, + payload_len: size_t, + sender_account_index: u64, + sender_account_address: *const u8, + transaction_sequence_number: Nonce, + block_timestamp: Timestamp, + remaining_energy: u64, + block_state_out: *mut *mut PersistentBlockState, + used_energy_out: *mut u64, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, data_out) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!(!payload.is_null(), "payload is a null pointer."); + assert!( + !block_state_out.is_null(), + "block_state_out is a null pointer." + ); + assert!( + !used_energy_out.is_null(), + "used_energy_out is a null pointer." + ); + assert!( + !sender_account_address.is_null(), + "sender_account_address is a null pointer." + ); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + let external = ExternalBlockStateOperationCallbacks { + queries: ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }, + update_token_account_balance_ptr: update_token_account_balance_callback, + touch_token_account_ptr: touch_token_account_callback, + increment_plt_update_sequence_number_ptr: increment_plt_update_sequence_number_callback, + _not_send_sync: PhantomData, + }; + let mut context = FfiSchedulerEntityContext { + external, + store: load_callback, + }; + let sender_account_index = AccountIndex::from(sender_account_index); + let sender_account_address = { + let mut address_bytes = [0u8; contracts_common::ACCOUNT_ADDRESS_SIZE]; + unsafe { + std::ptr::copy_nonoverlapping( + sender_account_address, + address_bytes.as_mut_ptr(), + contracts_common::ACCOUNT_ADDRESS_SIZE, + ); + } + AccountAddress(address_bytes) + }; + let payload_bytes = unsafe { std::slice::from_raw_parts(payload, payload_len) }; + let payload: Payload = common::from_bytes_complete(payload_bytes) + .expect("Failed decoding transaction payload"); + let remaining_energy = Energy::from(remaining_energy); + let transaction_context = TransactionContext { + sender_account_address, + transaction_sequence_number, + block_timestamp, + energy_limit: remaining_energy, + }; + let (result, new_block_state) = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let mut block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + ( + scheduler::p9::execute_transaction( + &mut context, + &mut block_state, + transaction_context, + Account::from_existing_account(sender_account_index), + payload, + ), + PersistentBlockState::P9(block_state.persistent), + ) + } + PersistentBlockState::P10(persistent) => { + let mut block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + ( + scheduler::p9::execute_transaction( + &mut context, + &mut block_state, + transaction_context, + Account::from_existing_account(sender_account_index), + payload, + ), + PersistentBlockState::P10(block_state.persistent), + ) + } + PersistentBlockState::P11(persistent) => { + assert!( + !external_chain_parameters.is_null(), + "external_chain_parameters is a null pointer for P11." + ); + let PersistentChainParameters::P11(p11_chain_parameters) = + unsafe { &*external_chain_parameters }; + let mut block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + ( + scheduler::p11::execute_transaction( + &mut context, + &mut block_state, + transaction_context, + Account::from_existing_account(sender_account_index), + payload, + p11_chain_parameters, + ), + PersistentBlockState::P11(block_state.persistent), + ) + } + }; + let summary = result.expect("Unexpected failure during transaction execution"); + unsafe { + *used_energy_out = summary.energy_used.energy; + } + match summary.outcome { + TransactionOutcome::Success(events) => { + unsafe { + *block_state_out = Box::into_raw(Box::new(new_block_state)); + } + (status::FfiStatusCode::Success, common::to_bytes(&events)) + } + TransactionOutcome::Rejected(reject_reason) => ( + status::FfiStatusCode::Failed, + common::to_bytes(&reject_reason), + ), + } + }); + + let array = memory::alloc_array_from_vec(data_out); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for calling [`scheduler::execute_chain_update`]. +/// +/// Returns a byte representing the result: +/// +/// - [`status::FfiStatusCode::Success`]: Chain update execution succeeded and update was applied to block state. +/// - [`status::FfiStatusCode::Failed`]: Chain update failed. Block state changes applied +/// via callbacks must be rolled back. +/// - [`status::FfiStatusCode::Panic`]: Execution of the chain update resulted in an unrecoverable error or panic. +/// +/// # Arguments +/// +/// - `load_callback` External function to call for loading bytes a reference from the blob store. +/// - `read_token_account_balance_callback` External function to call reading the token balance of an account. +/// - `update_token_account_balance_callback` External function to call updating the token balance of an account. +/// - `touch_token_account_callback` External function to call to touch token account state. +/// - `increment_plt_update_sequence_number_callback` External function for incrementing the PLT update instruction sequence number. +/// - `get_account_address_by_index_callback` External function for getting account canonical address by account index. +/// - `get_account_index_by_address_callback` External function for getting account index by account address. +/// - `get_token_account_states_callback` External function for getting token account states. +/// - `block_state` Shared pointer to a block state to use as input state to execution. +/// - `payload` Shared pointer to chain update payload bytes. +/// - `payload_len` Byte length of chain update payload. +/// - `block_state_out` Location for writing the pointer of the updated block state. +/// The block state is only written if return value is [`status::FfiStatusCode::Success`]. +/// The pointer written is to a uniquely owned instance. +/// The caller must free the written block state using `ffi_free_plt_block_state` when it is no longer used. +/// - `return_data_out` Location for writing pointer to array containing return data, which is either serialized events or a failure kind. +/// If the return value is [`status::FfiStatusCode::Success`], the data is a list of block item events. If the return value +/// is [`status::FfiStatusCode::Failed`], it is a failure kind. +/// The pointer written is to a uniquely owned array. +/// The caller must free the written array using `free_array_len_2` when it is no longer used. +/// - `return_data_len_out` Location for writing the length of the array whose pointer was written to `return_data_out`. +/// +/// # Safety +/// +/// - All callback arguments must be a valid function pointers to functions with a signature matching the +/// signature of Rust type of the function pointer. +/// - Argument `block_state` must be a non-null pointer to well-formed [`plt_block_state::block_state::BlockState`]. +/// The pointer is to a shared instance, hence only valid for reading (writing only allowed through interior mutability). +/// - Argument `payload` must be non-null and valid for reads for `payload_len` many bytes. +/// - Argument `block_state_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_out` must be a non-null and valid pointer for writing +/// - Argument `return_data_len_out` must be a non-null and valid pointer for writing +#[unsafe(no_mangle)] +extern "C" fn ffi_execute_chain_update( + load_callback: LoadCallback, + read_token_account_balance_callback: ReadTokenAccountBalanceCallback, + update_token_account_balance_callback: UpdateTokenAccountBalanceCallback, + touch_token_account_callback: TouchTokenAccountCallback, + increment_plt_update_sequence_number_callback: IncrementPltUpdateSequenceNumberCallback, + get_account_index_by_address_callback: GetAccountIndexByAddressCallback, + get_account_address_by_index_callback: GetCanonicalAddressByAccountIndexCallback, + get_token_account_states_callback: GetTokenAccountStatesCallback, + block_state: *const PersistentBlockState, + payload: *const u8, + payload_len: size_t, + block_state_out: *mut *mut PersistentBlockState, + return_data_out: *mut *mut u8, + return_data_len_out: *mut size_t, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(|| { + assert!(!block_state.is_null(), "block_state is a null pointer."); + assert!(!payload.is_null(), "payload is a null pointer."); + assert!( + !block_state_out.is_null(), + "block_state_out is a null pointer." + ); + assert!( + !return_data_len_out.is_null(), + "return_data_len_out is a null pointer." + ); + assert!( + !return_data_out.is_null(), + "return_data_out is a null pointer." + ); + + let external = ExternalBlockStateOperationCallbacks { + queries: ExternalBlockStateQueryCallbacks { + read_token_account_balance_ptr: read_token_account_balance_callback, + get_account_address_by_index_ptr: get_account_address_by_index_callback, + get_account_index_by_address_ptr: get_account_index_by_address_callback, + get_token_account_states_ptr: get_token_account_states_callback, + }, + update_token_account_balance_ptr: update_token_account_balance_callback, + touch_token_account_ptr: touch_token_account_callback, + increment_plt_update_sequence_number_ptr: increment_plt_update_sequence_number_callback, + _not_send_sync: PhantomData, + }; + let mut context = FfiSchedulerEntityContext { + external, + store: load_callback, + }; + let payload_bytes = unsafe { std::slice::from_raw_parts(payload, payload_len) }; + let payload: UpdatePayload = common::from_bytes_complete(payload_bytes) + .expect("Failed decoding chain update payload"); + let (result, new_block_state) = match unsafe { &*block_state } { + PersistentBlockState::P9(persistent) => { + let mut block_state = BlockStateP9 { + persistent: persistent.clone(), + }; + ( + scheduler::p9::execute_chain_update(&mut context, &mut block_state, payload), + PersistentBlockState::P9(block_state.persistent), + ) + } + PersistentBlockState::P10(persistent) => { + let mut block_state = BlockStateP10 { + persistent: persistent.clone(), + }; + ( + scheduler::p9::execute_chain_update(&mut context, &mut block_state, payload), + PersistentBlockState::P10(block_state.persistent), + ) + } + PersistentBlockState::P11(persistent) => { + let mut block_state = BlockStateP11 { + persistent: persistent.clone(), + }; + ( + scheduler::p11::execute_chain_update(&mut context, &mut block_state, payload), + PersistentBlockState::P11(block_state.persistent), + ) + } + }; + + let outcome = result.expect("Unexpected failure during chain update execution"); + match outcome { + ChainUpdateOutcome::Success(events) => { + unsafe { + *block_state_out = Box::into_raw(Box::new(new_block_state)); + } + (status::FfiStatusCode::Success, common::to_bytes(&events)) + } + ChainUpdateOutcome::Failed(failure_kind) => ( + status::FfiStatusCode::Failed, + common::to_bytes(&failure_kind), + ), + } + }); + + let array = memory::alloc_array_from_vec(return_data); + unsafe { + *return_data_len_out = array.length; + *return_data_out = array.array; + } + return_status +} + +/// C-binding for executing a chain update against external chain parameters. +/// +/// The current parameters are left unchanged. On success, `params_out` receives +/// a uniquely owned successor which the caller must free with +/// `ffi_free_external_chain_parameters`. +/// +/// # Safety +/// +/// - `params` must be non-null and point to well-formed [`PersistentChainParameters`]. +/// - `payload` must be non-null and valid for reads of `payload_len` bytes. +/// - `params_out` must be non-null and valid for writing. +#[unsafe(no_mangle)] +extern "C" fn ffi_execute_external_chain_parameters_update( + params: *const PersistentChainParameters, + payload: *const u8, + payload_len: size_t, + params_out: *mut *mut PersistentChainParameters, +) -> status::FfiStatusCode { + let (return_status, return_data) = status::catch_unwind(move || { + assert!(!params.is_null(), "params is a null pointer."); + assert!(!payload.is_null(), "payload is a null pointer."); + assert!(!params_out.is_null(), "params_out is a null pointer."); + let payload_bytes = unsafe { std::slice::from_raw_parts(payload, payload_len) }; + let payload: UpdatePayload = common::from_bytes_complete(payload_bytes) + .expect("Failed decoding external chain parameter update payload"); + let (result, updated_params) = match unsafe { &*params } { + PersistentChainParameters::P11(persistent) => { + let mut chain_parameters = persistent.clone(); + ( + scheduler::p11::execute_chain_parameters_update(&mut chain_parameters, payload), + PersistentChainParameters::P11(chain_parameters), + ) + } + }; + result.expect("Unexpected failure during external chain parameter update execution"); + unsafe { + *params_out = Box::into_raw(Box::new(updated_params)); + } + (status::FfiStatusCode::Success, Vec::new()) + }); + if !return_data.is_empty() { + eprintln!("{}", String::from_utf8_lossy(&return_data)); + } + return_status +} + +#[cfg(test)] +mod tests { + use plt_block_state::ffi::blob_store_callbacks::tests_helpers::UNIMPLEMENTED_LOAD_CALLBACK; + use plt_block_state::ffi::block_state_callbacks::tests_helpers::{ + UNIMPLEMENTED_GET_ACCOUNT_INDEX_BY_ADDRESS, + UNIMPLEMENTED_GET_CANONICAL_ADDRESS_BY_ACCOUNT_INDEX, + UNIMPLEMENTED_GET_TOKEN_ACCOUNT_STATES, UNIMPLEMENTED_INCREMENT_PLT_UPDATE_SEQUENCE_NUMBER, + UNIMPLEMENTED_READ_TOKEN_ACCOUNT_BALANCE, UNIMPLEMENTED_TOUCH_TOKEN_ACCOUNT, + UNIMPLEMENTED_UPDATE_TOKEN_ACCOUNT_BALANCE, + }; + + use super::*; + use concordium_base::protocol_level_tokens::{RawCbor, meta_operations::MetaUpdatePayload}; + use plt_scheduler_types::types::protocol_version::ProtocolVersion; + use std::ptr; + + /// Test ensuring panics are caught and returned properly when providing invalid arguments to `ffi_execute_transaction`. + #[test] + fn test_execute_transaction_catches_panic() { + let data_out = Box::into_raw(Box::new(ptr::null_mut())); + let data_out_len = Box::into_raw(Box::new(0)); + + let status_code = ffi_execute_transaction( + UNIMPLEMENTED_LOAD_CALLBACK, + UNIMPLEMENTED_READ_TOKEN_ACCOUNT_BALANCE, + UNIMPLEMENTED_UPDATE_TOKEN_ACCOUNT_BALANCE, + UNIMPLEMENTED_TOUCH_TOKEN_ACCOUNT, + UNIMPLEMENTED_INCREMENT_PLT_UPDATE_SEQUENCE_NUMBER, + UNIMPLEMENTED_GET_ACCOUNT_INDEX_BY_ADDRESS, + UNIMPLEMENTED_GET_CANONICAL_ADDRESS_BY_ACCOUNT_INDEX, + UNIMPLEMENTED_GET_TOKEN_ACCOUNT_STATES, + ptr::null(), + ptr::null(), + ptr::null(), + 0, + 0, + ptr::null(), + Nonce::from(1), + Timestamp::from_timestamp_millis(0), + 0, + ptr::null_mut(), + ptr::null_mut(), + data_out, + data_out_len, + ); + assert_eq!(status_code, status::FfiStatusCode::Panic); + let data = unsafe { + let data_len = *data_out_len; + assert!(data_len > 0); + let data_ptr = *data_out; + assert!(!data_ptr.is_null()); + std::slice::from_raw_parts(data_ptr, data_len) + }; + let message = std::str::from_utf8(data).expect("Failed decoding panic message"); + assert_eq!(message, "block_state is a null pointer."); + } + + #[test] + fn test_p11_transaction_requires_external_chain_parameters() { + let block_state = PersistentBlockState::empty(ProtocolVersion::P11); + let payload = common::to_bytes(&Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(vec![]), + }, + }); + let sender_address = [0; contracts_common::ACCOUNT_ADDRESS_SIZE]; + let block_state_out = Box::into_raw(Box::new(ptr::null_mut())); + let used_energy_out = Box::into_raw(Box::new(0)); + let data_out = Box::into_raw(Box::new(ptr::null_mut())); + let data_out_len = Box::into_raw(Box::new(0)); + + let status_code = ffi_execute_transaction( + UNIMPLEMENTED_LOAD_CALLBACK, + UNIMPLEMENTED_READ_TOKEN_ACCOUNT_BALANCE, + UNIMPLEMENTED_UPDATE_TOKEN_ACCOUNT_BALANCE, + UNIMPLEMENTED_TOUCH_TOKEN_ACCOUNT, + UNIMPLEMENTED_INCREMENT_PLT_UPDATE_SEQUENCE_NUMBER, + UNIMPLEMENTED_GET_ACCOUNT_INDEX_BY_ADDRESS, + UNIMPLEMENTED_GET_CANONICAL_ADDRESS_BY_ACCOUNT_INDEX, + UNIMPLEMENTED_GET_TOKEN_ACCOUNT_STATES, + &block_state, + ptr::null(), + payload.as_ptr(), + payload.len(), + 0, + sender_address.as_ptr(), + Nonce::from(1), + Timestamp::from_timestamp_millis(0), + 0, + block_state_out, + used_energy_out, + data_out, + data_out_len, + ); + assert_eq!(status_code, status::FfiStatusCode::Panic); + let data = unsafe { std::slice::from_raw_parts(*data_out, *data_out_len) }; + let message = std::str::from_utf8(data).expect("Failed decoding panic message"); + assert_eq!( + message, + "external_chain_parameters is a null pointer for P11." + ); + } +} diff --git a/plt/plt-scheduler/src/ffi/status.rs b/plt/plt-scheduler/src/ffi/status.rs new file mode 100644 index 0000000000..5f553f888e --- /dev/null +++ b/plt/plt-scheduler/src/ffi/status.rs @@ -0,0 +1,46 @@ +pub use plt_block_state::ffi::status::FfiStatusCode; + +/// Helper function for wrapping calls with [`std::panic::catch_unwind`] then mapping a panic to the +/// correct status code and extracting the panic message. +/// +/// # Arguments +/// +/// - `function` The closure which might panic +pub fn catch_unwind(function: F) -> (FfiStatusCode, Vec) +where + F: FnOnce() -> (FfiStatusCode, Vec) + std::panic::UnwindSafe, +{ + std::panic::catch_unwind(function).unwrap_or_else(|err| { + let data_out = if let Some(message) = err.downcast_ref::() { + message.clone().into_bytes() + } else if let Some(message) = err.downcast_ref::<&str>() { + message.to_string().into_bytes() + } else { + b"Unknown panic reason".to_vec() + }; + (FfiStatusCode::Panic, data_out) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_catch_unwind_panics_str() { + let (status, message) = catch_unwind(|| { + panic!("my panic message"); + }); + assert_eq!(status, FfiStatusCode::Panic); + assert_eq!(String::from_utf8(message).unwrap(), "my panic message"); + } + + #[test] + fn test_catch_unwind_panics_string() { + let (status, message) = catch_unwind(|| { + panic!("my panic message {:?}", vec![5]); + }); + assert_eq!(status, FfiStatusCode::Panic); + assert_eq!(String::from_utf8(message).unwrap(), "my panic message [5]"); + } +} diff --git a/plt/plt-scheduler/src/lib.rs b/plt/plt-scheduler/src/lib.rs new file mode 100644 index 0000000000..6c46571d6e --- /dev/null +++ b/plt/plt-scheduler/src/lib.rs @@ -0,0 +1,11 @@ +pub mod block_state_polymorph; +pub mod failure; +#[cfg(feature = "ffi")] +mod ffi; +pub mod protocol_level_locks; +pub mod protocol_level_tokens; +pub mod scheduler; +mod transaction_execution; + +pub use protocol_level_tokens::token_module::TOKEN_MODULE_REF; +pub use transaction_execution::TransactionContext; diff --git a/plt/plt-scheduler/src/protocol_level_locks.rs b/plt/plt-scheduler/src/protocol_level_locks.rs new file mode 100644 index 0000000000..fcd0a2d329 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_locks.rs @@ -0,0 +1,5 @@ +pub mod lock_configuration; +pub mod lock_controller; + +pub mod p11; +pub mod p9; diff --git a/plt/plt-scheduler/src/protocol_level_locks/lock_configuration.rs b/plt/plt-scheduler/src/protocol_level_locks/lock_configuration.rs new file mode 100644 index 0000000000..e52993f846 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_locks/lock_configuration.rs @@ -0,0 +1,58 @@ +use concordium_base::{ + protocol_level_locks::{LockConfig, LockRecipients}, + protocol_level_tokens::CborHolderAccount, +}; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::AccountNotFoundByIndexError; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_block_state::persistent::protocol_level_locks::p11::{ + LockConfiguration, LockRecipients as BlockStateLockRecipients, +}; + +/// Get the recipients for a lock configuration, resolving [`AccountIndex`]es +/// to [`CborHolderAccount`]s. +pub fn get_recipients( + context: &EntityContext, + configuration: &LockConfiguration, +) -> BlockStateResult { + match &configuration.recipients { + BlockStateLockRecipients::Any => Ok(LockRecipients::Any), + BlockStateLockRecipients::Limited(recipients) => { + let recipients = recipients + .iter() + .map(|account_index| { + let with_addr = context.account_by_index(*account_index).map_err( + |_err: AccountNotFoundByIndexError| { + BlockStateFailure::Invariant(format!( + "account index {} in lock recipients does not exist", + account_index + )) + }, + )?; + Ok(CborHolderAccount::from(with_addr.canonical_account_address)) + }) + .collect::, _>>()?; + + Ok(LockRecipients::Limited(recipients)) + } + } +} + +/// Get the lock configuration as a CBOR-representable [`LockConfig`] with +/// resolved account addresses. +pub fn get_lock_config( + context: &EntityContext, + configuration: &LockConfiguration, +) -> BlockStateResult { + let recipients = get_recipients(context, configuration)?; + let controller = + super::lock_controller::to_cbor_controller(context, &configuration.controller)?; + + Ok(LockConfig { + recipients, + expiry: configuration.expiry, + controller, + metadata: configuration.metadata.clone(), + }) +} diff --git a/plt/plt-scheduler/src/protocol_level_locks/lock_controller.rs b/plt/plt-scheduler/src/protocol_level_locks/lock_controller.rs new file mode 100644 index 0000000000..fece8d9140 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_locks/lock_controller.rs @@ -0,0 +1,191 @@ +//! Runtime interface for protocol-level lock controllers. + +use crate::failure::ResultWithBlockStateFailure; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_locks::LockControllerSimpleV0Capability; +use concordium_base::protocol_level_tokens::CborHolderAccount; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaLockCancelDetails, MetaLockFundDetails, MetaLockReturnDetails, MetaLockSendDetails, +}; +use plt_block_state::entity::accounts::{Account, Accounts}; +use plt_block_state::entity::block_state::TokenNotFoundByIdError; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::{AccountNotFoundByAddressError, AccountNotFoundByIndexError}; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_block_state::persistent::protocol_level_locks::p11::{ + LockControllerConfig, LockControllerSimpleV0, LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; + +/// Runtime lock operation model. This corresponds to the "fund", "send", "return", and "cancel" +/// CBOR operations for interacting with locks from concordium-base. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum LockOperation { + Fund(MetaLockFundDetails), + Send(MetaLockSendDetails), + Return(MetaLockReturnDetails), + Cancel(MetaLockCancelDetails), +} + +/// Approve or reject a lock operation. Returns `Ok(())` if the operation is authorized, or +/// a `TransactionRejectReason` if it is not. +/// +/// * `sender_address`: account address of the sender +/// * `sender`: the transaction sender reference +/// * `operation`: the lock operation to approve/reject. +pub fn validate_operation( + controller_config: &LockControllerConfig, + sender_address: AccountAddress, + sender: &Account, + operation: &LockOperation, +) -> Result<(), TransactionRejectReason> { + let LockControllerConfig::SimpleV0(controller_config) = controller_config; + + match operation { + LockOperation::Fund(fund_details) => { + if !controller_config.has_role( + sender.account_index(), + LockControllerSimpleV0Capability::Fund, + ) { + return Err(TransactionRejectReason::LockFundNotAuthorized( + fund_details.lock.clone(), + sender_address, + )); + } + if !controller_config.tokens.contains(&fund_details.token) { + return Err(TransactionRejectReason::LockTokenNotPermitted( + fund_details.lock.clone(), + fund_details.token.clone(), + )); + } + } + LockOperation::Send(send_details) => { + if !controller_config.has_role( + sender.account_index(), + LockControllerSimpleV0Capability::Send, + ) { + return Err(TransactionRejectReason::LockSendNotAuthorized( + send_details.lock.clone(), + sender_address, + )); + } + } + LockOperation::Return(return_details) => { + if !controller_config.has_role( + sender.account_index(), + LockControllerSimpleV0Capability::Return, + ) { + return Err(TransactionRejectReason::LockReturnNotAuthorized( + return_details.lock.clone(), + sender_address, + )); + } + } + LockOperation::Cancel(cancel_details) => { + if !controller_config.has_role( + sender.account_index(), + LockControllerSimpleV0Capability::Cancel, + ) { + return Err(TransactionRejectReason::LockCancelNotAuthorized( + cancel_details.lock.clone(), + sender_address, + )); + } + } + } + Ok(()) +} + +/// Construct this lock controller from the given configuration. +pub fn from_cbor_controller( + context: &EntityContext, + block_state: &BlockStateP11, + cbor_controller: concordium_base::protocol_level_locks::LockController, +) -> ResultWithBlockStateFailure { + let concordium_base::protocol_level_locks::LockController::SimpleV0(cbor_controller) = + cbor_controller; + + let grants = cbor_controller + .grants + .into_iter() + .map(|grant| { + let account = context.account_by_address(&grant.account.address).map_err( + |_err: AccountNotFoundByAddressError| { + TransactionRejectReason::InvalidAccountReference(grant.account.address) + }, + )?; + + Ok(LockControllerSimpleV0Grant { + account: account.account_index(), + roles: grant.roles, + }) + }) + .collect::>()?; + + let tokens = cbor_controller + .tokens + .into_iter() + .map(|token_id| { + // Check that token exists + let token = block_state.token_by_id(context, &token_id)?.map_err( + |_err: TokenNotFoundByIdError| { + TransactionRejectReason::NonExistentTokenId(token_id.clone()) + }, + )?; + + // Return canonical token id + Ok(token.token_p9_base.token_configuration(context)?.token_id) + }) + .collect::>()?; + + let lock_controller = LockControllerSimpleV0 { + grants, + tokens, + keep_alive: cbor_controller.keep_alive, + memo: cbor_controller.memo, + }; + + Ok(LockControllerConfig::SimpleV0(lock_controller)) +} + +/// Convert this controller configuration to its canonical CBOR +/// [`concordium_base::protocol_level_locks::LockController`] representation, used by the +/// `lock-info` payload returned from `query_lock_info`. +pub fn to_cbor_controller( + context: &EntityContext, + controller_config: &LockControllerConfig, +) -> BlockStateResult { + let LockControllerConfig::SimpleV0(controller_config) = controller_config; + + let grants = controller_config + .grants + .iter() + .map(|grant| { + let with_addr = context.account_by_index(grant.account).map_err( + |err: AccountNotFoundByIndexError| { + BlockStateFailure::Invariant(format!( + "Account persisted in lock controller grants not found: {}", + err + )) + }, + )?; + Ok( + concordium_base::protocol_level_locks::LockControllerSimpleV0Grant { + account: CborHolderAccount::from(with_addr.canonical_account_address), + roles: grant.roles.clone(), + }, + ) + }) + .collect::>()?; + Ok( + concordium_base::protocol_level_locks::LockController::SimpleV0( + concordium_base::protocol_level_locks::LockControllerSimpleV0 { + grants, + tokens: controller_config.tokens.clone(), + keep_alive: controller_config.keep_alive, + memo: controller_config.memo.clone(), + }, + ), + ) +} diff --git a/plt/plt-scheduler/src/protocol_level_locks/p11.rs b/plt/plt-scheduler/src/protocol_level_locks/p11.rs new file mode 100644 index 0000000000..6719fb0bac --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_locks/p11.rs @@ -0,0 +1,576 @@ +use crate::failure::{ResultWithBlockStateFailure, ResultWithBlockStateFailureExt}; +use crate::protocol_level_locks::{ + lock_configuration, lock_configuration::get_lock_config, lock_controller, +}; +use crate::protocol_level_tokens::token_module::check_transfer_constraints; +use crate::protocol_level_tokens::{balance_operations, reject, token_amount, token_module}; +use crate::transaction_execution::TransactionExecution; +use concordium_base::base::AccountIndex; +use concordium_base::common::cbor; +use concordium_base::contracts_common::Duration; +use concordium_base::protocol_level_locks::LockRecipients as CborLockRecipients; +use concordium_base::protocol_level_locks::{ + LockAccountFunds, LockId, LockInfo, LockedTokenAmount, +}; +use concordium_base::protocol_level_tokens::TokenAmount; +use concordium_base::protocol_level_tokens::meta_operations::{ + LockOperation, MetaLockCancelDetails, MetaLockCreateDetails, MetaLockFundDetails, + MetaLockReturnDetails, MetaLockSendDetails, +}; +use concordium_base::protocol_level_tokens::{CborHolderAccount, RawCbor}; +use concordium_base::transactions; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::block_state::LockNotFoundByIdError; +use plt_block_state::entity::block_state::TokenNotFoundByIdError; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::AccountNotFoundByIndexError; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_block_state::persistent::protocol_level_locks::p11::{ + LockConfiguration, LockControllerConfig, LockRecipients, +}; +use plt_scheduler_types::types::events::{self, BlockItemEvent}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::RawTokenAmount; +use std::collections::BTreeMap; + +/// Get the [`LockId`]s of all protocol-level locks registered on the chain at the +/// end of the block. +/// +/// NOTE: this is a naive implementation. We might need to optimize with a streaming solution +/// instead, to not load all locks in existence into memory all at once. +pub fn query_lock_list( + context: &EntityContext, + block_state: &BlockStateP11, +) -> BlockStateResult> { + block_state.lock_list(context) +} + +/// Query [`LockInfo`] a lock. +/// +/// The function builds the [`LockInfo`] from the locks static [`LockConfiguration`] and +/// the non-static per-`(account, token)` balances held by the lock. +pub fn query_lock_info( + context: &EntityContext, + block_state: &BlockStateP11, + lock_id: &LockId, +) -> ResultWithBlockStateFailure { + let lock = block_state.lock_by_id(context, lock_id)??; + let configuration = lock.lock_configuration(context)?; + + // Resolve recipients (block-state `AccountIndex`es) into `CborHolderAccount` values + // by looking up each account's canonical address. + let recipients = lock_configuration::get_recipients(context, &configuration)?; + + // Convert the lock controller configuration into the CBOR `LockController` shape used + // by the `lock-info` payload. Variant-specific resolution (e.g. expanding grant + // `AccountIndex`es to `CborHolderAccount`) lives on the per-variant + // `crate::locks::lock_controller::LockController` impl. + let controller = lock_controller::to_cbor_controller(context, &configuration.controller)?; + + // Group the tracked `(account, token)` balances by account so we emit a single + // `LockAccountFunds` entry per account. + let mut funds_by_account: BTreeMap> = BTreeMap::new(); + for (account_index, token_index) in lock.lock_balance_refs() { + let token = block_state.token_by_index(context, token_index)?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + // for each locked balance record for the lock, get the locked token amount recorded in the + // account state of the token. + let raw_balance = token_module::query_locked_balance( + context, + &token, + account_index, + &configuration.lock_id, + )?; + let amount = TokenAmount::from_raw(raw_balance.into(), token_configuration.decimals); + funds_by_account + .entry(account_index) + .or_default() + .push(LockedTokenAmount { + token: token_configuration.token_id, + amount, + }); + } + + // Resolve the account addresses for the accounts holding locked funds + let funds: Vec = funds_by_account + .into_iter() + .map(|(account_index, amounts)| { + let with_addr = context.account_by_index(account_index).map_err( + |_err: AccountNotFoundByIndexError| { + BlockStateFailure::Invariant(format!( + "account index {} returned by `lock_balances` does not exist", + account_index + )) + }, + )?; + Ok(LockAccountFunds { + account: CborHolderAccount::from(with_addr.canonical_account_address), + amounts, + }) + }) + .collect::>()?; + + let lock_info = LockInfo { + lock: configuration.lock_id.clone(), + recipients, + expiry: configuration.expiry, + controller, + metadata: configuration.metadata.clone(), + funds, + }; + + Ok(RawCbor::from(cbor::cbor_encode(&lock_info))) +} + +/// Execute [`LockOperation`]. +pub fn execute_lock_operation( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP11, + max_lock_duration: Duration, + operation_index: usize, + lock_operation: LockOperation, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + match lock_operation { + LockOperation::Fund(details) => execute_lock_fund( + context, + transaction_execution, + block_state, + operation_index, + details, + events, + ), + LockOperation::Send(details) => execute_lock_send( + context, + transaction_execution, + block_state, + operation_index, + details, + events, + ), + LockOperation::Return(details) => execute_lock_return( + context, + transaction_execution, + block_state, + operation_index, + details, + events, + ), + LockOperation::Create(details) => execute_lock_create( + context, + transaction_execution, + block_state, + max_lock_duration, + details, + events, + ), + LockOperation::Cancel(details) => { + execute_lock_cancel(context, transaction_execution, block_state, details, events) + } + } +} + +fn execute_lock_fund( + context: &mut EntityContext, + transaction_execution: &TransactionExecution, + block_state: &mut BlockStateP11, + operation_index: usize, + details: MetaLockFundDetails, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + // TODO: (COR-2306) charge. + let mut lock = block_state + .lock_by_id(context, &details.lock)? + .map_err(|err| TransactionRejectReason::NonExistentLockId(err.0))?; + + let lock_configuration = lock.lock_configuration(context)?; + if lock_configuration + .expiry + .is_expired(transaction_execution.timestamp()) + { + return Err( + TransactionRejectReason::LockExpired(lock_configuration.lock_id.clone()).into(), + ); + } + + lock_controller::validate_operation( + &lock_configuration.controller, + transaction_execution.sender_account_address(), + transaction_execution.sender_account(), + &lock_controller::LockOperation::Fund(details.clone()), + )?; + + let mut token = block_state.token_by_id(context, &details.token)?.map_err( + |TokenNotFoundByIdError(token_id)| TransactionRejectReason::NonExistentTokenId(token_id), + )?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + let raw_amount = token_amount::to_raw_token_amount(&token_configuration, details.amount) + .map_err(|err| { + reject::deserialization_failure_amount_decimals_mismatch(&token_configuration, err) + })?; + + let memo = details.memo.map(transactions::Memo::from); + let is_new_holder = balance_operations::lock_amount( + context, + events, + &mut token, + transaction_execution.sender_account(), + transaction_execution.sender_account_address(), + &lock_configuration.lock_id, + raw_amount, + memo, + ) + .map_nested_err(|err| { + reject::insufficient_balance(&token_configuration, operation_index, err) + })?; + + let token_index = token.token_p9_base.token_index(); + block_state.update_token(context, token)?; + + if is_new_holder { + lock.add_lock_balance_ref( + transaction_execution.sender_account().account_index(), + token_index, + ); + block_state.update_lock(context, lock)?; + } + Ok(()) +} + +fn execute_lock_send( + context: &mut EntityContext, + transaction_execution: &TransactionExecution, + block_state: &mut BlockStateP11, + operation_index: usize, + details: MetaLockSendDetails, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + // TODO: (COR-2306) charge. + let lock = block_state + .lock_by_id(context, &details.lock)? + .map_err(|err| TransactionRejectReason::NonExistentLockId(err.0))?; + + let lock_configuration = lock.lock_configuration(context)?; + if lock_configuration + .expiry + .is_expired(transaction_execution.timestamp()) + { + return Err( + TransactionRejectReason::LockExpired(lock_configuration.lock_id.clone()).into(), + ); + } + + let source_address = details.source.address; + let source = context + .account_by_address(&source_address) + .map_err(|_| TransactionRejectReason::InvalidAccountReference(source_address))?; + let recipient_address = details.recipient.address; + let recipient = context + .account_by_address(&recipient_address) + .map_err(|_| TransactionRejectReason::InvalidAccountReference(recipient_address))?; + + let mut token = block_state.token_by_id(context, &details.token)?.map_err( + |TokenNotFoundByIdError(token_id)| TransactionRejectReason::NonExistentTokenId(token_id), + )?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + check_transfer_constraints( + context, + &token.token_p9_base, + &source, + source_address, + &recipient, + recipient_address, + operation_index, + )?; + + if !lock_configuration + .recipients + .is_recipient(&recipient.account_index()) + { + return Err(TransactionRejectReason::LockRecipientNotPermitted( + lock_configuration.lock_id.clone(), + recipient_address, + ) + .into()); + } + + lock_controller::validate_operation( + &lock_configuration.controller, + transaction_execution.sender_account_address(), + transaction_execution.sender_account(), + &lock_controller::LockOperation::Send(details.clone()), + )?; + + let raw_amount = token_amount::to_raw_token_amount(&token_configuration, details.amount) + .map_err(|err| { + reject::deserialization_failure_amount_decimals_mismatch(&token_configuration, err) + })?; + + let memo = details.memo.map(transactions::Memo::from); + let remaining_locked = balance_operations::send_locked_amount( + context, + events, + &mut token, + &source, + source_address, + &recipient, + recipient_address, + &lock_configuration.lock_id, + raw_amount, + memo, + ) + .map_nested_err(|err| { + reject::insufficient_balance(&token_configuration, operation_index, err) + })?; + + let token_index = token.token_p9_base.token_index(); + block_state.update_token(context, token)?; + + if remaining_locked == RawTokenAmount::from(0) { + remove_lock_balance_ref( + context, + block_state, + events, + lock_configuration_keeps_alive(&lock_configuration), + lock, + source.account_index(), + token_index, + details.lock, + )?; + } + + Ok(()) +} + +fn execute_lock_return( + context: &mut EntityContext, + transaction_execution: &TransactionExecution, + block_state: &mut BlockStateP11, + operation_index: usize, + details: MetaLockReturnDetails, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + // TODO: (COR-2306) charge. + let lock = block_state + .lock_by_id(context, &details.lock)? + .map_err(|err| TransactionRejectReason::NonExistentLockId(err.0))?; + + let lock_configuration = lock.lock_configuration(context)?; + if lock_configuration + .expiry + .is_expired(transaction_execution.timestamp()) + { + return Err( + TransactionRejectReason::LockExpired(lock_configuration.lock_id.clone()).into(), + ); + } + + let source_address = details.source.address; + let source = context + .account_by_address(&source_address) + .map_err(|_| TransactionRejectReason::InvalidAccountReference(source_address))?; + + lock_controller::validate_operation( + &lock_configuration.controller, + transaction_execution.sender_account_address(), + transaction_execution.sender_account(), + &lock_controller::LockOperation::Return(details.clone()), + )?; + + let mut token = block_state.token_by_id(context, &details.token)?.map_err( + |TokenNotFoundByIdError(token_id)| TransactionRejectReason::NonExistentTokenId(token_id), + )?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + let raw_amount = token_amount::to_raw_token_amount(&token_configuration, details.amount) + .map_err(|err| { + reject::deserialization_failure_amount_decimals_mismatch(&token_configuration, err) + })?; + + let memo = details.memo.map(transactions::Memo::from); + let remaining_locked = balance_operations::return_locked_amount( + context, + events, + &mut token, + source.account_index(), + source_address, + &lock_configuration.lock_id, + raw_amount, + memo, + ) + .map_nested_err(|err| { + reject::insufficient_balance(&token_configuration, operation_index, err) + })?; + + let token_index = token.token_p9_base.token_index(); + block_state.update_token(context, token)?; + + if remaining_locked == RawTokenAmount::from(0) { + remove_lock_balance_ref( + context, + block_state, + events, + lock_configuration_keeps_alive(&lock_configuration), + lock, + source.account_index(), + token_index, + details.lock, + )?; + } + + Ok(()) +} + +fn execute_lock_create( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP11, + max_lock_duration: Duration, + details: MetaLockCreateDetails, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + let account_index = transaction_execution.sender_account().account_index(); + let sequence_number = transaction_execution.transaction_sequence_number(); + let creation_order = transaction_execution.next_lock_creation_order(); + let lock_id = LockId::new(account_index, sequence_number, creation_order); + let concordium_base::protocol_level_locks::LockConfig { + recipients, + expiry, + controller: controller_config, + metadata, + } = details.config; + + let transaction_timestamp = transaction_execution.timestamp(); + + if expiry.is_expired(transaction_timestamp) { + return Err(TransactionRejectReason::LockExpired(lock_id).into()); + } + + let Some(expiry_millis) = expiry.seconds.checked_mul(1000) else { + return Err(TransactionRejectReason::LockDurationTooLong(lock_id).into()); + }; + + if expiry_millis - transaction_timestamp.timestamp_millis() > max_lock_duration.millis() { + return Err(TransactionRejectReason::LockDurationTooLong(lock_id).into()); + } + + let controller = + lock_controller::from_cbor_controller(context, block_state, controller_config)?; + + let recipients = match recipients { + CborLockRecipients::Any => LockRecipients::Any, + CborLockRecipients::Limited(recipients) => { + let recipients = recipients + .into_iter() + .map( + |recipient| match context.account_by_address(&recipient.address) { + Ok(account) => Ok(account.account_index()), + Err(_) => Err(TransactionRejectReason::InvalidAccountReference( + recipient.address, + )), + }, + ) + .collect::, TransactionRejectReason>>()?; + LockRecipients::from(recipients) + } + }; + let configuration = LockConfiguration { + lock_id: lock_id.clone(), + recipients, + expiry, + controller, + metadata, + }; + + let config = get_lock_config(context, &configuration)?; + let event = events::LockCreateEvent { + lock_id: lock_id.clone(), + lock_config: RawCbor::from(cbor::cbor_encode(&config)), + }; + events.push(BlockItemEvent::LockCreated(event)); + + block_state.create_lock(context, configuration)?; + Ok(()) +} + +fn execute_lock_cancel( + context: &mut EntityContext, + transaction_execution: &TransactionExecution, + block_state: &mut BlockStateP11, + details: MetaLockCancelDetails, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + // TODO: (COR-2306) charge. + let lock = block_state + .lock_by_id(context, &details.lock)? + .map_err(|err| TransactionRejectReason::NonExistentLockId(err.0))?; + + let lock_configuration = lock.lock_configuration(context)?; + let memo: Option = details.memo.clone().map(transactions::Memo::from); + + if !lock_configuration + .expiry + .is_expired(transaction_execution.timestamp()) + { + lock_controller::validate_operation( + &lock_configuration.controller, + transaction_execution.sender_account_address(), + transaction_execution.sender_account(), + &lock_controller::LockOperation::Cancel(details), + )?; + } + for (account_index, token_index) in lock.lock_balance_refs() { + let mut token = block_state.token_by_index(context, token_index)?; + balance_operations::unlock_balance( + context, + events, + &mut token, + account_index, + &lock_configuration.lock_id, + &memo, + )?; + block_state.update_token(context, token)?; + } + block_state.delete_lock(context, &lock_configuration.lock_id)?; + let event = events::LockDestroyEvent { + lock_id: lock_configuration.lock_id.clone(), + }; + events.push(BlockItemEvent::LockDestroyed(event)); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn remove_lock_balance_ref( + context: &EntityContext, + block_state: &mut BlockStateP11, + events: &mut Vec, + lock_keeps_alive: bool, + mut lock: plt_block_state::entity::protocol_level_locks::p11::LockP11, + account_index: AccountIndex, + token_index: plt_block_state::persistent::protocol_level_tokens::p9::TokenIndex, + lock_id: LockId, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + if !lock.remove_lock_balance_ref(account_index, token_index) { + // No lock state change needed: either the account still holds a non-zero balance + // controlled by the lock, or there was no balance reference to remove. + return Ok(()); + } + if lock.lock_balance_refs().is_empty() && !lock_keeps_alive { + block_state.delete_lock(context, &lock_id)?; + events.push(BlockItemEvent::LockDestroyed(events::LockDestroyEvent { + lock_id, + })); + } else { + block_state.update_lock(context, lock)?; + } + Ok(()) +} + +fn lock_configuration_keeps_alive(configuration: &LockConfiguration) -> bool { + match &configuration.controller { + LockControllerConfig::SimpleV0(controller) => controller.keep_alive, + } +} diff --git a/plt/plt-scheduler/src/protocol_level_locks/p9.rs b/plt/plt-scheduler/src/protocol_level_locks/p9.rs new file mode 100644 index 0000000000..b8ea75ca1b --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_locks/p9.rs @@ -0,0 +1,27 @@ +use crate::failure::{ResultWithBlockStateFailure, WithBlockStateFailure}; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::RawCbor; +use plt_block_state::entity::block_state::LockNotFoundByIdError; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; + +/// Get the [`LockId`]s of all protocol-level locks registered on the chain at the +/// end of the block. +pub fn query_lock_list( + _context: &EntityContext, + _block_state: &BlockStateP9, +) -> BlockStateResult> { + Ok(vec![]) +} + +/// Assemble the [`LockInfo`] CBOR payload for a lock. +pub fn query_lock_info( + _context: &EntityContext, + _block_state: &BlockStateP9, + lock_id: &LockId, +) -> ResultWithBlockStateFailure { + Err(WithBlockStateFailure::Error(LockNotFoundByIdError( + lock_id.clone(), + ))) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens.rs b/plt/plt-scheduler/src/protocol_level_tokens.rs new file mode 100644 index 0000000000..03ff8a26e5 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens.rs @@ -0,0 +1,6 @@ +pub mod balance_operations; +pub mod p11; +pub mod p9; +pub mod reject; +pub mod token_amount; +pub mod token_module; diff --git a/plt/plt-scheduler/src/protocol_level_tokens/balance_operations.rs b/plt/plt-scheduler/src/protocol_level_tokens/balance_operations.rs new file mode 100644 index 0000000000..d532dbe738 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/balance_operations.rs @@ -0,0 +1,510 @@ +use crate::block_state_polymorph::token::{TokenPXRef, TokenPXRefMut}; +use crate::failure::{HigherLevelProtocolError, ResultWithBlockStateFailure}; +use concordium_base::base::AccountIndex; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::transactions::Memo; +use plt_block_state::entity::accounts::{Account, Accounts}; +use plt_block_state::entity::protocol_level_tokens::p9::TokenP9Base; +use plt_block_state::entity::protocol_level_tokens::p11::TokenP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::{OverflowError, RawTokenAmountDelta}; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_scheduler_types::types::events::{ + BlockItemEvent, TokenBurnEvent, TokenMintEvent, TokenTransferEvent, +}; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenAmount, TokenHolder}; + +/// The account has insufficient balance. +#[derive(Debug, thiserror::Error)] +#[error("Insufficient balance on account")] +pub struct InsufficientBalanceError { + /// Balance available on account + pub available: RawTokenAmount, + /// Balance required on account + pub required: RawTokenAmount, +} + +/// Mint exceed the representable amount. +#[derive(Debug, thiserror::Error)] +#[error("Minting the requested amount would overflow the circulating supply amount")] +pub struct MintWouldOverflowError { + /// Amount requested to be minted + pub requested_amount: RawTokenAmount, + /// Current circulating supply of the token + pub current_supply: RawTokenAmount, + /// Maximum representable token amount + pub max_representable_amount: RawTokenAmount, +} + +impl HigherLevelProtocolError for InsufficientBalanceError {} +impl HigherLevelProtocolError for MintWouldOverflowError {} + +/// Get the available balance for an account. +/// +/// For protocol versions without locks this is the total account balance. +/// For protocol versions with locks this is the total account balance minus the +/// sum of all locked balances. +/// +/// # Errors +/// +/// Returns a [`BlockStateFailure::Invariant`] if the sum of locked balances +/// overflows or exceeds the total account balance. +pub fn available_balance( + context: &EntityContext, + token: TokenPXRef<'_>, + account: &Account, +) -> BlockStateResult { + let total = account.account_token_balance(context, token.token_p9_base().token_index()); + + let TokenPXRef::TokenP11(token) = token else { + return Ok(total); + }; + + let mut total_locked = RawTokenAmount::from(0); + for (_, locked_balance) in token + .get_locked_balances_for_account(context, account.account_index())? + .into_iter() + { + total_locked = total_locked.checked_add(locked_balance).ok_or_else(|| { + BlockStateFailure::Invariant("Total locked token balance overflow".to_string()) + })?; + } + + total.checked_sub(total_locked).ok_or_else(|| { + BlockStateFailure::Invariant( + "Total locked token balance exceeds account token balance".to_string(), + ) + }) +} + +/// Mint a specified amount and deposit it in the account. +/// +/// # Events +/// +/// This will produce a `TokenMintEvent` in the logs. +/// +/// # Errors +/// +/// - [`MintWouldOverflowError`] The total supply would exceed the representable amount. +pub fn mint( + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP9Base, + account: &Account, + account_address: AccountAddress, + amount: RawTokenAmount, +) -> ResultWithBlockStateFailure<(), MintWouldOverflowError> { + let token_configuration = token.token_configuration(context)?; + + // Update total supply + let new_circulating_supply = + token + .token_circulating_supply() + .checked_add(amount) + .ok_or(MintWouldOverflowError { + requested_amount: amount, + current_supply: token.token_circulating_supply(), + max_representable_amount: RawTokenAmount::MAX, + })?; + + token.set_token_circulating_supply(new_circulating_supply); + + // Update balance of the account + account + .update_token_account_balance( + context, + token.token_index(), + RawTokenAmountDelta::Add(amount), + ) + .map_err(|_err: OverflowError| { + // We should never overflow account balance at mint, since the total circulating supply of the token + // is always less that what is representable as a token amount. + BlockStateFailure::Invariant("Mint destination account amount overflow".to_string()) + })?; + + // Issue event + let event = BlockItemEvent::TokenMint(TokenMintEvent { + token_id: token_configuration.token_id.clone(), + target: TokenHolder::Account(account_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + }); + + events.extend(Some(event)); + + Ok(()) +} + +/// Burn a specified amount from the account. +/// +/// # Events +/// +/// This will produce a `TokenBurnEvent` in the logs. +/// +/// # Errors +/// +/// - [`InsufficientBalanceError`] The sender has insufficient balance. +pub fn burn( + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + account: &Account, + account_address: AccountAddress, + amount: RawTokenAmount, +) -> ResultWithBlockStateFailure<(), InsufficientBalanceError> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + let available = available_balance(context, token.as_ref(), account)?; + if amount > available { + return Err(InsufficientBalanceError { + available, + required: amount, + } + .into()); + } + + // Update balance of the account + account + .update_token_account_balance( + context, + token.token_p9_base().token_index(), + RawTokenAmountDelta::Subtract(amount), + ) + .map_err(|_err: OverflowError| { + BlockStateFailure::Invariant( + "Available token balance check passed, but burn underflowed".to_string(), + ) + })?; + + // Update total supply + let new_circulating_supply = token + .token_p9_base() + .token_circulating_supply() + .checked_sub(amount) + .ok_or_else(|| + // We should never overflow total supply at burn, since the total circulating supply of the token + // is always more than any account balance. + BlockStateFailure::Invariant( + "Circulating supply amount overflow at burn".to_string(), + ))?; + token + .token_p9_base_mut() + .set_token_circulating_supply(new_circulating_supply); + + // Issue event + let event = BlockItemEvent::TokenBurn(TokenBurnEvent { + token_id: token_configuration.token_id.clone(), + target: TokenHolder::Account(account_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + }); + + events.extend(Some(event)); + + Ok(()) +} + +/// 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. +#[allow(clippy::too_many_arguments)] +pub fn transfer( + context: &mut EntityContext, + events: &mut impl Extend, + token: TokenPXRefMut<'_>, + from: &Account, + from_address: AccountAddress, + to: &Account, + to_address: AccountAddress, + amount: RawTokenAmount, + memo: Option, +) -> ResultWithBlockStateFailure<(), InsufficientBalanceError> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + let available = available_balance(context, token.as_ref(), from)?; + if amount > available { + return Err(InsufficientBalanceError { + available, + required: amount, + } + .into()); + } + + // Update sender balance + from.update_token_account_balance( + context, + token.token_p9_base().token_index(), + RawTokenAmountDelta::Subtract(amount), + ) + .map_err(|_err: OverflowError| { + BlockStateFailure::Invariant( + "Available token balance check passed, but transfer underflowed".to_string(), + ) + })?; + + // Update receiver balance + to.update_token_account_balance( + context, + token.token_p9_base().token_index(), + RawTokenAmountDelta::Add(amount), + ) + .map_err(|_err: OverflowError| { + // We should never overflow at transfer, since the total circulating supply of the token + // is always less that what is representable as a token amount. + BlockStateFailure::Invariant("Transfer destination token amount overflow".to_string()) + })?; + + // Issue event + let event = BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: token_configuration.token_id.clone(), + from: TokenHolder::Account(from_address), + to: TokenHolder::Account(to_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + memo, + from_lock: None, + to_lock: None, + }); + + events.extend(Some(event)); + + Ok(()) +} + +/// Move `amount` of tokens from an account's available balance into the control of a lock. +/// The tokens remain on the account but become locked. +/// +/// Returns `true` if the account had no locked balance for this lock before — i.e. a new +/// `(account, lock)` balance relationship was created — and `false` if the account already +/// held a non-zero locked balance for the lock. +/// +/// # Events +/// +/// Produces a [`TokenTransferEvent`] with `to_lock` set to the lock id. +/// +/// # Errors +/// +/// - [`InsufficientBalanceError`] The account has insufficient available balance. +#[allow(clippy::too_many_arguments)] +pub fn lock_amount( + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + account: &Account, + account_address: AccountAddress, + lock_id: &LockId, + amount: RawTokenAmount, + memo: Option, +) -> ResultWithBlockStateFailure { + let available = available_balance(context, TokenPXRef::TokenP11(token), account)?; + if amount > available { + return Err(InsufficientBalanceError { + available, + required: amount, + } + .into()); + } + + let old_locked = + token.get_locked_balance_for_account(context, account.account_index(), lock_id)?; + let new_locked = old_locked.checked_add(amount).ok_or_else(|| { + BlockStateFailure::Invariant("Locked balance overflow at fund".to_string()) + })?; + token.set_locked_balance_for_account(context, account.account_index(), lock_id, new_locked)?; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + events.extend(Some(BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: token_configuration.token_id, + from: TokenHolder::Account(account_address), + to: TokenHolder::Account(account_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + memo, + from_lock: None, + to_lock: Some(lock_id.clone()), + }))); + + Ok(old_locked == RawTokenAmount::from(0) && new_locked > RawTokenAmount::from(0)) +} + +/// Move `amount` of tokens from a lock's control on `source` to `recipient`'s available balance. +/// +/// Returns the remaining locked balance for `source` after the operation. A return value of +/// zero indicates the `(source, lock)` balance relationship should be removed by the caller. +/// +/// # Events +/// +/// Produces a [`TokenTransferEvent`] with `from_lock` set to the lock id. +/// +/// # Errors +/// +/// - [`InsufficientBalanceError`] The source has insufficient locked balance. +#[allow(clippy::too_many_arguments)] +pub fn send_locked_amount( + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + source: &Account, + source_address: AccountAddress, + recipient: &Account, + recipient_address: AccountAddress, + lock_id: &LockId, + amount: RawTokenAmount, + memo: Option, +) -> ResultWithBlockStateFailure { + let old_locked = + token.get_locked_balance_for_account(context, source.account_index(), lock_id)?; + let new_locked = old_locked + .checked_sub(amount) + .ok_or(InsufficientBalanceError { + available: old_locked, + required: amount, + })?; + token.set_locked_balance_for_account(context, source.account_index(), lock_id, new_locked)?; + + source + .update_token_account_balance( + context, + token.token_p9_base.token_index(), + RawTokenAmountDelta::Subtract(amount), + ) + .map_err(|_err: OverflowError| { + BlockStateFailure::Invariant("Transfer source token amount overflow".to_string()) + })?; + recipient + .update_token_account_balance( + context, + token.token_p9_base.token_index(), + RawTokenAmountDelta::Add(amount), + ) + .map_err(|_err: OverflowError| { + BlockStateFailure::Invariant("Transfer destination token amount overflow".to_string()) + })?; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + events.extend(Some(BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: token_configuration.token_id, + from: TokenHolder::Account(source_address), + to: TokenHolder::Account(recipient_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + memo, + from_lock: Some(lock_id.clone()), + to_lock: None, + }))); + + Ok(new_locked) +} + +/// Release `amount` from a lock's control back to the owner account's available balance. +/// The tokens remain on the account but are freed from lock control. +/// +/// Returns the remaining locked balance for `account` after the operation. A return value of +/// zero indicates the `(account, lock)` balance relationship should be removed by the caller. +/// +/// # Events +/// +/// Produces a [`TokenTransferEvent`] with `from_lock` set to the lock id. +/// +/// # Errors +/// +/// - [`InsufficientBalanceError`] The account has insufficient locked balance. +#[allow(clippy::too_many_arguments)] +pub fn return_locked_amount( + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + account_index: AccountIndex, + account_address: AccountAddress, + lock_id: &LockId, + amount: RawTokenAmount, + memo: Option, +) -> ResultWithBlockStateFailure { + let old_locked = token.get_locked_balance_for_account(context, account_index, lock_id)?; + let new_locked = old_locked + .checked_sub(amount) + .ok_or(InsufficientBalanceError { + available: old_locked, + required: amount, + })?; + token.set_locked_balance_for_account(context, account_index, lock_id, new_locked)?; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + events.extend(Some(BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: token_configuration.token_id, + from: TokenHolder::Account(account_address), + to: TokenHolder::Account(account_address), + amount: TokenAmount { + amount, + decimals: token_configuration.decimals, + }, + memo, + from_lock: Some(lock_id.clone()), + to_lock: None, + }))); + + Ok(new_locked) +} + +/// Unlock the balance of an account associated with a particular lock for +/// this particular token. This generates a `TokenTransferEvent` to reflect +/// the change in the locked balance. +pub fn unlock_balance( + context: &EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + account_index: AccountIndex, + lock_id: &LockId, + memo: &Option, +) -> BlockStateResult<()> { + let old_balance = token.get_locked_balance_for_account(context, account_index, lock_id)?; + if old_balance == RawTokenAmount::from(0) { + // No locked balance, nothing to do. + return Ok(()); + } + token.set_locked_balance_for_account( + context, + account_index, + lock_id, + RawTokenAmount::from(0), + )?; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + let account_address = context + .account_by_index(account_index) + .map_err(|err| { + BlockStateFailure::Invariant(format!("Account not found by index: {}", err)) + })? + .canonical_account_address; + events.extend(Some(BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: token_configuration.token_id, + from: TokenHolder::Account(account_address), + to: TokenHolder::Account(account_address), + amount: TokenAmount { + amount: old_balance, + decimals: token_configuration.decimals, + }, + memo: memo.clone(), + from_lock: Some(lock_id.clone()), + to_lock: None, + }))); + + Ok(()) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/p11.rs b/plt/plt-scheduler/src/protocol_level_tokens/p11.rs new file mode 100644 index 0000000000..1ce79356f4 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/p11.rs @@ -0,0 +1,331 @@ +use crate::block_state_polymorph::token::{TokenPXRef, TokenPXRefMut}; +use crate::failure::{ResultWithBlockStateFailure, ResultWithBlockStateFailureExt}; +use crate::protocol_level_tokens::reject; +use crate::transaction_execution::{OutOfEnergyError, TransactionExecution}; +use crate::{TOKEN_MODULE_REF, protocol_level_tokens::token_module}; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + RawCbor, TokenId, TokenModuleInitializationParameters, TokenOperation, TokenOperations, + TokenOperationsPayload, +}; +use concordium_base::transactions; +use concordium_base::updates::CreatePlt; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::TokenNotFoundByIdError; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_block_state::utils; +use plt_scheduler_types::types::events::{BlockItemEvent, TokenCreateEvent}; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, FailureKind, TransactionOutcome}; +use plt_scheduler_types::types::queries::{ + TokenAccountInfo, TokenAccountState, TokenAuthorizations, TokenInfo, TokenState, +}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::TokenAmount; + +/// Get the [`TokenId`]s of all protocol-level tokens registered on the chain. +pub fn query_plt_list( + context: &EntityContext, + block_state: &BlockStateP11, +) -> BlockStateResult> { + block_state.plt_list(context) +} + +/// Get the token state associated with the given token id. +pub fn query_token_info( + context: &EntityContext, + block_state: &BlockStateP11, + token_id: &TokenId, +) -> ResultWithBlockStateFailure { + let token = block_state.token_by_id(context, token_id)??; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + let circulating_supply = token.token_p9_base.token_circulating_supply(); + + let total_supply = TokenAmount { + amount: circulating_supply, + decimals: token_configuration.decimals, + }; + + let module_state = token_module::query_token_module_state(context, &token.token_p9_base)?; + + let token_state = TokenState { + token_module_ref: token_configuration.module_ref, + decimals: token_configuration.decimals, + total_supply, + module_state: RawCbor::from(cbor::cbor_encode(&module_state)), + }; + + let token_info = TokenInfo { + // The token configuration contains the canonical token id specified in the original casing + token_id: token_configuration.token_id, + state: token_state, + }; + + Ok(token_info) +} + +/// Get the list of tokens on an account +pub fn query_token_account_infos( + context: &EntityContext, + block_state: &BlockStateP11, + account: Account, +) -> BlockStateResult> { + account + .token_account_states(context) + .map(|(token_index, state)| { + let token = block_state.token_by_index(context, token_index)?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let module_state = token_module::query_token_module_account_state( + context, + TokenPXRef::TokenP11(&token), + account.account_index(), + state.balance, + )?; + + let balance = TokenAmount { + amount: state.balance, + decimals: token_configuration.decimals, + }; + + let account_state = TokenAccountState { + balance, + module_state: Some(RawCbor::from(cbor::cbor_encode(&module_state))), + }; + + Ok(TokenAccountInfo { + token_id: token_configuration.token_id, + account_state, + }) + }) + .collect() +} + +/// Get the authorizations of a token. +pub fn query_token_authorizations( + context: &EntityContext, + block_state: &BlockStateP11, + token_id: &TokenId, +) -> ResultWithBlockStateFailure { + let token = block_state.token_by_id(context, token_id)??; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let details = token_module::query_token_authorizations(context, &token)?; + + Ok(TokenAuthorizations { + token_id: token_configuration.token_id, + details: RawCbor::from(cbor::cbor_encode(&details)), + }) +} + +/// Execute a create protocol-level token chain update modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a failure kind is returned. +/// +/// NOTICE: The caller must ensure to rollback state changes in case a failure kind is returned. +/// +/// # Arguments +/// +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The create PLT chain update. +/// +/// # Errors +/// +/// - [`ChainUpdateExecutionError`] If executing the update instruction failed. +/// Returning this error will terminate the scheduler. +pub fn execute_create_plt_chain_update( + context: &mut EntityContext, + block_state: &mut BlockStateP11, + payload: CreatePlt, +) -> BlockStateResult { + // Check that token id is not already used (notice that token_by_id lookup is case-insensitive + // as the check should be) + if let Ok(existing_token) = block_state.token_by_id(context, &payload.token_id)? { + return Ok(ChainUpdateOutcome::Failed(FailureKind::DuplicateTokenId( + existing_token + .token_p9_base + .token_configuration(context)? + .token_id, + ))); + } + + // Check token module ref matches the implemented token module + if payload.token_module != TOKEN_MODULE_REF { + return Ok(ChainUpdateOutcome::Failed( + FailureKind::InvalidTokenModuleRef(payload.token_module), + )); + } + + let token_configuration = TokenConfiguration { + token_id: payload.token_id.clone(), + module_ref: payload.token_module, + decimals: payload.decimals, + }; + + // Create token in block state + let token_index = block_state.create_token(context, token_configuration.clone())?; + let mut token = block_state.token_by_index(context, token_index)?; + + let mut events = Vec::new(); + events.push(BlockItemEvent::TokenCreated(TokenCreateEvent { + payload: payload.clone(), + })); + + let initialization_parameters: TokenModuleInitializationParameters = + match utils::cbor_decode(payload.initialization_parameters) { + Ok(parameters) => parameters, + Err(err) => { + return Ok(ChainUpdateOutcome::Failed( + FailureKind::TokenInitializeFailure(format!( + "Could not decode token initialization parameters: {}", + err + )), + )); + } + }; + + // Initialize token in token module + let token_initialize_result = token_module::initialize_token( + context, + &mut events, + TokenPXRefMut::TokenP11(&mut token), + &initialization_parameters, + ) + .nest()?; + + match token_initialize_result { + Ok(()) => { + // Increment protocol-level token update sequence number + block_state.increment_plt_update_instruction_sequence_number(context); + + block_state.update_token(context, token)?; + + // Return events + Ok(ChainUpdateOutcome::Success(events)) + } + Err(err) => Ok(ChainUpdateOutcome::Failed( + FailureKind::TokenInitializeFailure(err.to_string()), + )), + } +} + +/// Execute a token update transaction payload modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a reject reason. +/// Energy must be charged during execution by calling [`TransactionExecution::tick_energy`]. If +/// execution is out of energy, the function `tick_energy` returns an error which means execution must be stopped, +/// and the [`OutOfEnergy`](TransactionRejectReason::OutOfEnergy) reject reason must be returned. +/// +/// NOTICE: The caller must ensure to rollback state changes in case of the transaction being rejected. +/// +/// # Arguments +/// +/// - `transaction_execution` Context of transaction execution that allows accessing sending account +/// and charging energy. +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The token update transaction payload to execute. +/// +/// # Errors +/// +/// - [`TransactionExecutionError`] If executing the transaction fails with an unrecoverable error. +/// Returning this error will terminate the scheduler. +pub fn execute_token_update_transaction( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP11, + payload: TokenOperationsPayload, +) -> BlockStateResult { + // Charge energy + if let Err(err) = + transaction_execution.tick_energy(transactions::cost::PLT_OPERATIONS_TRANSACTIONS) + { + let _: OutOfEnergyError = err; // assert type of error + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::OutOfEnergy, + )); + } + + // Lookup token + let mut token = match block_state.token_by_id(context, &payload.token_id)? { + Ok(token) => token, + Err(TokenNotFoundByIdError(_)) => { + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::NonExistentTokenId(payload.token_id), + )); + } + }; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let mut events = Vec::new(); + + // Decode operations + let operations: TokenOperations = match utils::cbor_decode(payload.operations) { + Ok(operations) => operations, + Err(err) => { + return Ok(TransactionOutcome::Rejected( + reject::deserialization_failure(&token_configuration, err), + )); + } + }; + + // Execute operations + for (index, operation) in operations.operations.into_iter().enumerate() { + match token_module::execute_token_update_operation_at_index( + transaction_execution, + context, + &mut events, + TokenPXRefMut::TokenP11(&mut token), + index, + &operation, + ) + .nest()? + { + Ok(()) => (), + Err(reject_reason) => { + return Ok(TransactionOutcome::Rejected(reject_reason)); + } + }; + } + + // Write back the token + block_state.update_token(context, token)?; + + // Return events + Ok(TransactionOutcome::Success(events)) +} + +/// Execute [`TokenOperation`] +pub fn execute_token_update_operation( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP11, + operation_index: usize, + token_id: &TokenId, + token_operation: TokenOperation, + events: &mut Vec, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + // Lookup token + let mut token = + block_state + .token_by_id(context, token_id)? + .map_err(|_err: TokenNotFoundByIdError| { + TransactionRejectReason::NonExistentTokenId(token_id.clone()) + })?; + + // Execute operation + token_module::execute_token_update_operation_at_index( + transaction_execution, + context, + events, + TokenPXRefMut::TokenP11(&mut token), + operation_index, + &token_operation, + )?; + + // Write back the token + block_state.update_token(context, token)?; + + Ok(()) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/p9.rs b/plt/plt-scheduler/src/protocol_level_tokens/p9.rs new file mode 100644 index 0000000000..bfe5dbfd01 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/p9.rs @@ -0,0 +1,297 @@ +use crate::block_state_polymorph::token::{TokenPXRef, TokenPXRefMut}; +use crate::failure::{ResultWithBlockStateFailure, ResultWithBlockStateFailureExt}; +use crate::protocol_level_tokens::reject; +use crate::transaction_execution::{OutOfEnergyError, TransactionExecution}; +use crate::{TOKEN_MODULE_REF, protocol_level_tokens::token_module}; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + RawCbor, TokenId, TokenModuleInitializationParameters, TokenOperations, TokenOperationsPayload, +}; +use concordium_base::transactions; +use concordium_base::updates::CreatePlt; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::TokenNotFoundByIdError; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_block_state::utils; +use plt_scheduler_types::types::events::{BlockItemEvent, TokenCreateEvent}; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, FailureKind, TransactionOutcome}; +use plt_scheduler_types::types::queries::{ + TokenAccountInfo, TokenAccountState, TokenAuthorizations, TokenInfo, TokenState, +}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::TokenAmount; + +/// Get the [`TokenId`]s of all protocol-level tokens registered on the chain. +pub fn query_plt_list( + context: &EntityContext, + block_state: &BlockStateP9, +) -> BlockStateResult> { + block_state.plt_list(context).collect() +} + +/// Get the token state associated with the given token id. +pub fn query_token_info( + context: &EntityContext, + block_state: &BlockStateP9, + token_id: &TokenId, +) -> ResultWithBlockStateFailure { + let token = block_state.token_by_id(context, token_id)??; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + let circulating_supply = token.token_p9_base.token_circulating_supply(); + + let total_supply = TokenAmount { + amount: circulating_supply, + decimals: token_configuration.decimals, + }; + + let module_state = token_module::query_token_module_state(context, &token.token_p9_base)?; + + let token_state = TokenState { + token_module_ref: token_configuration.module_ref, + decimals: token_configuration.decimals, + total_supply, + module_state: RawCbor::from(cbor::cbor_encode(&module_state)), + }; + + let token_info = TokenInfo { + // The token configuration contains the canonical token id specified in the original casing + token_id: token_configuration.token_id, + state: token_state, + }; + + Ok(token_info) +} + +/// Get the list of tokens on an account +pub fn query_token_account_infos( + context: &EntityContext, + block_state: &BlockStateP9, + account: Account, +) -> BlockStateResult> { + account + .token_account_states(context) + .map(|(token_index, state)| { + let token = block_state.token_by_index(context, token_index)?; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let module_state = token_module::query_token_module_account_state( + context, + TokenPXRef::TokenP9(&token), + account.account_index(), + state.balance, + )?; + + let balance = TokenAmount { + amount: state.balance, + decimals: token_configuration.decimals, + }; + + let account_state = TokenAccountState { + balance, + module_state: Some(RawCbor::from(cbor::cbor_encode(&module_state))), + }; + + Ok(TokenAccountInfo { + token_id: token_configuration.token_id, + account_state, + }) + }) + .collect() +} + +/// Get the authorizations of a token. +pub fn query_token_authorizations( + context: &EntityContext, + block_state: &BlockStateP9, + token_id: &TokenId, +) -> ResultWithBlockStateFailure { + let token = block_state.token_by_id(context, token_id)??; + + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let details = concordium_base::protocol_level_tokens::TokenAuthorizations::default(); + + Ok(TokenAuthorizations { + token_id: token_configuration.token_id, + details: RawCbor::from(cbor::cbor_encode(&details)), + }) +} + +/// Execute a create protocol-level token chain update modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a failure kind is returned. +/// +/// NOTICE: The caller must ensure to rollback state changes in case a failure kind is returned. +/// +/// # Arguments +/// +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The create PLT chain update. +/// +/// # Errors +/// +/// - [`ChainUpdateExecutionError`] If executing the update instruction failed. +/// Returning this error will terminate the scheduler. +pub fn execute_create_plt_chain_update( + context: &mut EntityContext, + block_state: &mut BlockStateP9, + payload: CreatePlt, +) -> BlockStateResult { + // Check that token id is not already used (notice that token_by_id lookup is case-insensitive + // as the check should be) + if let Ok(existing_token) = block_state.token_by_id(context, &payload.token_id)? { + return Ok(ChainUpdateOutcome::Failed(FailureKind::DuplicateTokenId( + existing_token + .token_p9_base + .token_configuration(context)? + .token_id, + ))); + } + + // Check token module ref matches the implemented token module + if payload.token_module != TOKEN_MODULE_REF { + return Ok(ChainUpdateOutcome::Failed( + FailureKind::InvalidTokenModuleRef(payload.token_module), + )); + } + + let token_configuration = TokenConfiguration { + token_id: payload.token_id.clone(), + module_ref: payload.token_module, + decimals: payload.decimals, + }; + + // Create token in block state + let token_index = block_state.create_token(context, token_configuration.clone())?; + let mut token = block_state.token_by_index(context, token_index)?; + + let mut events = Vec::new(); + events.push(BlockItemEvent::TokenCreated(TokenCreateEvent { + payload: payload.clone(), + })); + + let initialization_parameters: TokenModuleInitializationParameters = + match utils::cbor_decode(payload.initialization_parameters) { + Ok(parameters) => parameters, + Err(err) => { + return Ok(ChainUpdateOutcome::Failed( + FailureKind::TokenInitializeFailure(format!( + "Could not decode token initialization parameters: {}", + err + )), + )); + } + }; + + // Initialize token in token module + let token_initialize_result = token_module::initialize_token( + context, + &mut events, + TokenPXRefMut::TokenP9(&mut token), + &initialization_parameters, + ) + .nest()?; + + match token_initialize_result { + Ok(()) => { + // Increment protocol-level token update sequence number + block_state.increment_plt_update_instruction_sequence_number(context); + + // Write back the token + block_state.update_token(context, token)?; + + // Return events + Ok(ChainUpdateOutcome::Success(events)) + } + Err(err) => Ok(ChainUpdateOutcome::Failed( + FailureKind::TokenInitializeFailure(err.to_string()), + )), + } +} + +/// Execute a token update transaction payload modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a reject reason. +/// Energy must be charged during execution by calling [`TransactionExecution::tick_energy`]. If +/// execution is out of energy, the function `tick_energy` returns an error which means execution must be stopped, +/// and the [`OutOfEnergy`](TransactionRejectReason::OutOfEnergy) reject reason must be returned. +/// +/// NOTICE: The caller must ensure to rollback state changes in case of the transaction being rejected. +/// +/// # Arguments +/// +/// - `transaction_execution` Context of transaction execution that allows accessing sending account +/// and charging energy. +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The token update transaction payload to execute. +/// +/// # Errors +/// +/// - [`TransactionExecutionError`] If executing the transaction fails with an unrecoverable error. +/// Returning this error will terminate the scheduler. +pub fn execute_token_update_transaction( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP9, + payload: TokenOperationsPayload, +) -> BlockStateResult { + // Charge energy + if let Err(err) = + transaction_execution.tick_energy(transactions::cost::PLT_OPERATIONS_TRANSACTIONS) + { + let _: OutOfEnergyError = err; // assert type of error + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::OutOfEnergy, + )); + } + + // Lookup token + let mut token = match block_state.token_by_id(context, &payload.token_id)? { + Ok(token) => token, + Err(TokenNotFoundByIdError(_)) => { + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::NonExistentTokenId(payload.token_id), + )); + } + }; + let token_configuration = token.token_p9_base.token_configuration(context)?; + + let mut events = Vec::new(); + + // Decode operations + let operations: TokenOperations = match utils::cbor_decode(payload.operations) { + Ok(operations) => operations, + Err(err) => { + return Ok(TransactionOutcome::Rejected( + reject::deserialization_failure(&token_configuration, err), + )); + } + }; + + // Execute operations + for (index, operation) in operations.operations.into_iter().enumerate() { + match token_module::execute_token_update_operation_at_index( + transaction_execution, + context, + &mut events, + TokenPXRefMut::TokenP9(&mut token), + index, + &operation, + ) + .nest()? + { + Ok(()) => (), + Err(reject_reason) => { + return Ok(TransactionOutcome::Rejected(reject_reason)); + } + }; + } + + // Write back the token + block_state.update_token(context, token)?; + + // Return events + Ok(TransactionOutcome::Success(events)) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/reject.rs b/plt/plt-scheduler/src/protocol_level_tokens/reject.rs new file mode 100644 index 0000000000..67ac755895 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/reject.rs @@ -0,0 +1,157 @@ +//! Constructors for [`TransactionRejectReason`]s created by the token module. +//! +//! The token module reject reasons are represented as a [`TokenModuleRejectReason`] inside +//! the variant [`TransactionRejectReason::TokenUpdateTransactionFailed`], hence their construction +//! is more elaborate than other transaction reject reasons. + +use crate::protocol_level_tokens::balance_operations::{ + InsufficientBalanceError, MintWouldOverflowError, +}; +use crate::protocol_level_tokens::token_amount; +use crate::protocol_level_tokens::token_amount::TokenAmountDecimalsMismatchError; +use concordium_base::common::cbor::CborSerializationError; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::{ + AddressNotFoundRejectReason, CborHolderAccount, DeserializationFailureRejectReason, + MintWouldOverflowRejectReason, OperationNotPermittedRejectReason, + TokenBalanceInsufficientRejectReason, TokenModuleRejectReason, + UnsupportedOperationRejectReason, +}; +use plt_block_state::external::AccountNotFoundByAddressError; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_scheduler_types::types::reject_reasons::{ + EncodedTokenModuleRejectReason, TransactionRejectReason, +}; + +pub fn address_not_found( + token_configuration: &TokenConfiguration, + operation_index: usize, + err: AccountNotFoundByAddressError, +) -> TransactionRejectReason { + let reject = TokenModuleRejectReason::AddressNotFound(AddressNotFoundRejectReason { + index: operation_index as u64, + address: CborHolderAccount::from(err.0), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn insufficient_balance( + token_configuration: &TokenConfiguration, + operation_index: usize, + err: InsufficientBalanceError, +) -> TransactionRejectReason { + let reject = + TokenModuleRejectReason::TokenBalanceInsufficient(TokenBalanceInsufficientRejectReason { + index: operation_index as u64, + available_balance: token_amount::to_token_amount(token_configuration, err.available), + required_balance: token_amount::to_token_amount(token_configuration, err.required), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn mint_would_overflow( + token_configuration: &TokenConfiguration, + operation_index: usize, + err: MintWouldOverflowError, +) -> TransactionRejectReason { + let reject = TokenModuleRejectReason::MintWouldOverflow(MintWouldOverflowRejectReason { + index: operation_index as u64, + requested_amount: token_amount::to_token_amount(token_configuration, err.requested_amount), + current_supply: token_amount::to_token_amount(token_configuration, err.current_supply), + max_representable_amount: token_amount::to_token_amount( + token_configuration, + err.max_representable_amount, + ), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn operation_not_permitted( + token_configuration: &TokenConfiguration, + operation_index: usize, + account_address: Option, + reason: String, +) -> TransactionRejectReason { + let reject = + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: operation_index as u64, + address: account_address.map(Into::into), + reason: Some(reason), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn operation_not_permitted_paused( + token_configuration: &TokenConfiguration, + operation_index: usize, + operation_type: &'static str, +) -> TransactionRejectReason { + let reject = + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: operation_index as u64, + address: None, + reason: format!("token operation {operation_type} is paused") + .to_string() + .into(), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn unsupported_operation( + token_configuration: &TokenConfiguration, + operation_index: usize, + operation_type: &'static str, + reason: String, +) -> TransactionRejectReason { + let reject = TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: operation_index as u64, + operation_type: operation_type.to_string(), + reason: Some(reason), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn deserialization_failure( + token_configuration: &TokenConfiguration, + err: CborSerializationError, +) -> TransactionRejectReason { + let reject = + TokenModuleRejectReason::DeserializationFailure(DeserializationFailureRejectReason { + cause: Some(err.to_string()), + }); + + token_module_reject(token_configuration, reject) +} + +pub fn deserialization_failure_amount_decimals_mismatch( + token_configuration: &TokenConfiguration, + err: TokenAmountDecimalsMismatchError, +) -> TransactionRejectReason { + let reject = + TokenModuleRejectReason::DeserializationFailure(DeserializationFailureRejectReason { + cause: Some(err.to_string()), + }); + + token_module_reject(token_configuration, reject) +} + +/// Generic constructor for creating a [`TransactionRejectReason`] +/// from [`TokenModuleRejectReason`]. +pub fn token_module_reject( + token_configuration: &TokenConfiguration, + reject_reason: TokenModuleRejectReason, +) -> TransactionRejectReason { + let (reason_type, cbor) = reject_reason.encode_reject_reason(); + TransactionRejectReason::TokenUpdateTransactionFailed(EncodedTokenModuleRejectReason { + // Use the canonical token id from the token configuration + token_id: token_configuration.token_id.clone(), + reason_type: reason_type.to_type_discriminator(), + details: Some(cbor), + }) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/token_amount.rs b/plt/plt-scheduler/src/protocol_level_tokens/token_amount.rs new file mode 100644 index 0000000000..9f655aca14 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/token_amount.rs @@ -0,0 +1,43 @@ +//! Utilities for handing token amount, specifically conversion between [`TokenAmount`] and +//! [`RawTokenAmount`]. + +use crate::failure::HigherLevelProtocolError; +use concordium_base::protocol_level_tokens::TokenAmount; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Token amount decimals mismatch +#[derive(Debug, thiserror::Error)] +#[error("Token amount decimals mismatch: expected {expected}, found {found}")] +pub struct TokenAmountDecimalsMismatchError { + /// Expected decimals + pub expected: u8, + /// Actual decimals + pub found: u8, +} + +impl HigherLevelProtocolError for TokenAmountDecimalsMismatchError {} + +/// Checks that token amount has the right number of decimals and converts it to a plain +/// integer and return [`RawTokenAmount`] +pub fn to_raw_token_amount( + token_configuration: &TokenConfiguration, + amount: TokenAmount, +) -> Result { + let kernel_decimals = token_configuration.decimals; + if amount.decimals() != kernel_decimals { + Err(TokenAmountDecimalsMismatchError { + expected: kernel_decimals, + found: amount.decimals(), + }) + } else { + Ok(RawTokenAmount::from(amount.value())) + } +} + +pub fn to_token_amount( + token_configuration: &TokenConfiguration, + amount: RawTokenAmount, +) -> TokenAmount { + TokenAmount::from_raw(amount.into(), token_configuration.decimals) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/token_module/initialize.rs b/plt/plt-scheduler/src/protocol_level_tokens/token_module/initialize.rs new file mode 100644 index 0000000000..0801647c83 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/token_module/initialize.rs @@ -0,0 +1,115 @@ +use crate::block_state_polymorph::token::TokenPXRefMut; +use crate::failure::{ + HigherLevelProtocolError, ResultWithBlockStateFailure, ResultWithBlockStateFailureExt, +}; +use crate::protocol_level_tokens::balance_operations::MintWouldOverflowError; +use crate::protocol_level_tokens::token_amount::TokenAmountDecimalsMismatchError; +use crate::protocol_level_tokens::{balance_operations, token_amount}; +use concordium_base::common::cbor::CborSerializationError; +use concordium_base::protocol_level_tokens::{TokenAdminRole, TokenModuleInitializationParameters}; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::AccountNotFoundByAddressError; +use plt_scheduler_types::types::events::BlockItemEvent; + +/// Represents the reasons why [`initialize_token`] can fail. +#[derive(Debug, thiserror::Error)] +pub enum TokenInitializationError { + #[error("Token initialization parameters could not be deserialized: {0}")] + InvalidInitializationParameters(String), + #[error("CBOR serialization error during token initialization: {0}")] + CborSerialization(#[from] CborSerializationError), + #[error("The given governance account does not exist: {0}")] + GovernanceAccountDoesNotExist(#[from] AccountNotFoundByAddressError), + #[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] MintWouldOverflowError), +} + +impl HigherLevelProtocolError for TokenInitializationError {} + +/// List roles which are unaffected by which features are enabled. +const UNIVERSAL_ROLES: &[TokenAdminRole] = &[ + TokenAdminRole::UpdateAdminRoles, + TokenAdminRole::Pause, + TokenAdminRole::UpdateMetadata, +]; + +/// 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( + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + init_params: &TokenModuleInitializationParameters, +) -> ResultWithBlockStateFailure<(), TokenInitializationError> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + let name = init_params.name.as_ref().ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token name is missing".to_string(), + ) + })?; + let metadata = init_params.metadata.as_ref().ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token metadata is missing".to_string(), + ) + })?; + let cbor_governance_account = init_params.governance_account.as_ref().ok_or_else(|| { + TokenInitializationError::InvalidInitializationParameters( + "Token governance account is missing".to_string(), + ) + })?; + token.token_p9_base_mut().set_token_name(context, name)?; + token + .token_p9_base_mut() + .set_metadata_url(context, metadata)?; + + // The governance account should hold every role, except for disabled features, so we build a + // list of every enabled role and the mandatory roles. + let mut enabled_roles = Vec::from(UNIVERSAL_ROLES); + + if init_params.allow_list == Some(true) { + token.token_p9_base_mut().set_allow_list_enabled(context)?; + enabled_roles.push(TokenAdminRole::UpdateAllowList); + } + if init_params.deny_list == Some(true) { + token.token_p9_base_mut().set_deny_list_enabled(context)?; + enabled_roles.push(TokenAdminRole::UpdateDenyList); + } + if init_params.mintable == Some(true) { + token.token_p9_base_mut().set_mintable_enabled(context)?; + enabled_roles.push(TokenAdminRole::Mint); + } + if init_params.burnable == Some(true) { + token.token_p9_base_mut().set_burnable_enabled(context)?; + enabled_roles.push(TokenAdminRole::Burn); + } + + let governance_account = context.account_by_address(&cbor_governance_account.address)?; + let governance_account_index = governance_account.account_index(); + token + .token_p9_base_mut() + .set_governance_account(context, governance_account_index)?; + + if let Some(initial_supply) = init_params.initial_supply { + let mint_amount = token_amount::to_raw_token_amount(&token_configuration, initial_supply)?; + + balance_operations::mint( + context, + events, + token.token_p9_base_mut(), + &governance_account, + cbor_governance_account.address, + mint_amount, + ) + .map_nested_err(Into::into)?; + } + + if let TokenPXRefMut::TokenP11(token) = token { + token.assign_account_roles(context, governance_account_index, &enabled_roles)?; + } + + Ok(()) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/token_module/mod.rs b/plt/plt-scheduler/src/protocol_level_tokens/token_module/mod.rs new file mode 100644 index 0000000000..0554968e7c --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/token_module/mod.rs @@ -0,0 +1,22 @@ +//! Implementation of the protocol-level token module. The token module implements +//! execution of [token operations](concordium_base::protocol_level_tokens::TokenOperation). +//! +//! It is the responsibility of the token module to charge energy for execution via +//! [`TransactionExecution`](crate::transaction_execution::TransactionExecution), +//! and release control (return an error) if the energy limit is reached. + +use concordium_base::protocol_level_tokens::TokenModuleRef; + +mod initialize; +mod queries; +mod update; + +pub use initialize::*; +pub use queries::*; +pub use update::*; + +/// Module ref for the currently implemented token module. It is the SHA-256 of "TokenModuleV0" +pub const TOKEN_MODULE_REF: TokenModuleRef = TokenModuleRef::new([ + 0x5c, 0x5c, 0x26, 0x45, 0xdb, 0x84, 0xa7, 0x02, 0x6d, 0x78, 0xf2, 0x50, 0x17, 0x40, 0xf6, 0x0a, + 0x8c, 0xcb, 0x8f, 0xae, 0x5c, 0x16, 0x6d, 0xc2, 0x42, 0x80, 0x77, 0xfd, 0x9a, 0x69, 0x9a, 0x4a, +]); diff --git a/plt/plt-scheduler/src/protocol_level_tokens/token_module/queries.rs b/plt/plt-scheduler/src/protocol_level_tokens/token_module/queries.rs new file mode 100644 index 0000000000..760303e175 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/token_module/queries.rs @@ -0,0 +1,178 @@ +use crate::block_state_polymorph::token::TokenPXRef; +use concordium_base::base::AccountIndex; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::{ + AccountLockAmount, CborHolderAccount, TokenAdminRole, TokenAmount, TokenAuthorizations, + TokenModuleAccountState, TokenModuleState, TokenRoleAuthorizations, +}; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::protocol_level_tokens::p9::TokenP9Base; +use plt_block_state::entity::protocol_level_tokens::p11::TokenP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::AccountNotFoundByIndexError; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Get the CBOR-encoded representation of the token module state. +pub fn query_token_module_state( + context: &EntityContext, + token: &TokenP9Base, +) -> BlockStateResult { + let governance_account_index = token.get_governance_account_index(context)?; + let governance_account = context.account_by_index(governance_account_index).map_err( + |_: AccountNotFoundByIndexError| { + BlockStateFailure::Invariant(format!( + "Stored governance account with index {} does not exist", + governance_account_index + )) + }, + )?; + + let state = TokenModuleState { + name: Some(token.get_token_name(context)?), + metadata: Some(token.get_metadata(context)?), + governance_account: Some(CborHolderAccount::from( + governance_account.canonical_account_address, + )), + allow_list: Some(token.has_allow_list(context)), + deny_list: Some(token.has_deny_list(context)), + mintable: Some(token.is_mintable(context)), + burnable: Some(token.is_burnable(context)), + paused: Some(token.is_paused(context)), + }; + + Ok(state) +} + +/// Get the CBOR-encoded representation of the token module account state. +pub fn query_token_module_account_state( + context: &EntityContext, + token: TokenPXRef<'_>, + account: AccountIndex, + total_token_balance: RawTokenAmount, +) -> BlockStateResult { + let token_base = token.token_p9_base(); + let has_allow_list = token_base.has_allow_list(context); + let allow_list = if has_allow_list { + token_base.get_allow_list_for(context, account).into() + } else { + None + }; + let has_deny_list = token_base.has_deny_list(context); + let deny_list = if has_deny_list { + token_base.get_deny_list_for(context, account).into() + } else { + None + }; + + let token_configuration = token_base.token_configuration(context)?; + + let mut total_locked = RawTokenAmount::from(0); + let mut locks = Vec::new(); + + if let TokenPXRef::TokenP11(token_p11) = token { + for (lock, locked_balance) in token_p11 + .get_locked_balances_for_account(context, account)? + .into_iter() + { + if locked_balance == RawTokenAmount::from(0) { + continue; + } + total_locked = total_locked.checked_add(locked_balance).ok_or_else(|| { + BlockStateFailure::Invariant("Total locked token balance overflow".to_string()) + })?; + locks.push(AccountLockAmount { + lock, + amount: TokenAmount::from_raw(locked_balance.into(), token_configuration.decimals), + }); + } + } + + let available = if total_locked == RawTokenAmount::from(0) { + None + } else { + let available = total_token_balance + .checked_sub(total_locked) + .ok_or_else(|| { + BlockStateFailure::Invariant( + "Total locked token balance exceeds account token balance".to_string(), + ) + })?; + Some(TokenAmount::from_raw( + available.into(), + token_configuration.decimals, + )) + }; + + Ok(TokenModuleAccountState { + allow_list, + deny_list, + locks, + available, + }) +} + +/// Get authorization roles and assigned accounts for the token. +pub fn query_token_authorizations( + context: &EntityContext, + token: &TokenP11, +) -> BlockStateResult { + let mut update_admin_roles = TokenRoleAuthorizations::default(); + let mut mint = TokenRoleAuthorizations::default(); + let mut burn = TokenRoleAuthorizations::default(); + let mut update_allow_list = TokenRoleAuthorizations::default(); + let mut update_deny_list = TokenRoleAuthorizations::default(); + let mut pause = TokenRoleAuthorizations::default(); + let mut update_metadata = TokenRoleAuthorizations::default(); + + for (account_index, roles) in token.all_roles(context)?.into_iter() { + let account = context + .account_by_index(account_index) + .map_err(|err| { + BlockStateFailure::Invariant(format!( + "Stored account index in authorizations cannot be found: {}", + err + )) + })? + .canonical_account_address; + + for role in roles.iter_assigned() { + match role { + TokenAdminRole::UpdateAdminRoles => { + update_admin_roles.accounts.push(account.into()) + } + TokenAdminRole::Mint => mint.accounts.push(account.into()), + TokenAdminRole::Burn => burn.accounts.push(account.into()), + TokenAdminRole::UpdateAllowList => update_allow_list.accounts.push(account.into()), + TokenAdminRole::UpdateDenyList => update_deny_list.accounts.push(account.into()), + TokenAdminRole::Pause => pause.accounts.push(account.into()), + TokenAdminRole::UpdateMetadata => update_metadata.accounts.push(account.into()), + } + } + } + Ok(TokenAuthorizations { + update_admin_roles: Some(update_admin_roles), + mint: token.token_p9_base.is_mintable(context).then_some(mint), + burn: token.token_p9_base.is_burnable(context).then_some(burn), + update_allow_list: token + .token_p9_base + .has_allow_list(context) + .then_some(update_allow_list), + update_deny_list: token + .token_p9_base + .has_deny_list(context) + .then_some(update_deny_list), + pause: Some(pause), + update_metadata: Some(update_metadata), + }) +} + +/// Get the locked balance of `account` under `lock` for the token in context. +pub fn query_locked_balance( + context: &EntityContext, + token: &TokenP11, + account: AccountIndex, + lock_id: &LockId, +) -> BlockStateResult { + token.get_locked_balance_for_account(context, account, lock_id) +} diff --git a/plt/plt-scheduler/src/protocol_level_tokens/token_module/update.rs b/plt/plt-scheduler/src/protocol_level_tokens/token_module/update.rs new file mode 100644 index 0000000000..e2ea04e0d5 --- /dev/null +++ b/plt/plt-scheduler/src/protocol_level_tokens/token_module/update.rs @@ -0,0 +1,916 @@ +use crate::block_state_polymorph::token::{TokenPXRef, TokenPXRefMut}; +use crate::failure::{ResultWithBlockStateFailure, ResultWithBlockStateFailureExt}; +use crate::protocol_level_tokens::balance_operations::{ + InsufficientBalanceError, MintWouldOverflowError, +}; +use crate::protocol_level_tokens::token_amount::TokenAmountDecimalsMismatchError; +use crate::protocol_level_tokens::{balance_operations, reject, token_amount}; +use crate::transaction_execution::{OutOfEnergyError, TransactionExecution}; +use concordium_base::base::Energy; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::{ + MetadataUrl, TokenAdminRole, TokenListUpdateDetails, TokenListUpdateEventDetails, + TokenModuleEvent, TokenOperation, TokenPauseEventDetails, TokenSupplyUpdateDetails, + TokenTransfer, TokenUpdateAdminRolesDetails, TokenUpdateAdminRolesEventDetails, + TokenUpdateMetadataEventDetails, +}; +use concordium_base::transactions::Memo; +use plt_block_state::entity::accounts::{Account, Accounts}; +use plt_block_state::entity::protocol_level_tokens::p9::TokenP9Base; +use plt_block_state::entity::protocol_level_tokens::p11::TokenP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::external::AccountNotFoundByAddressError; +use plt_block_state::failure::{BlockStateFailure, BlockStateResult}; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenConfiguration; +use plt_scheduler_types::types::events::{BlockItemEvent, EncodedTokenModuleEvent}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; + +/// Execute a token update operation using the token context to +/// update state and produce events. +/// +/// The caller must ensure to rollback state changes in case of +/// an error is returned. +/// +/// The following checks and operations are performed: +/// +/// - For a transfer operation: +/// +/// - Check that the transfer amount is specified with the correct number of decimals. +/// - Tick energy required for the operation. +/// - Check that the module is not paused. +/// - Check that the recipient is valid. +/// - Check allow and deny list restrictions. +/// - Transfer the amount from the sender to the recipient, if the sender's balance is +/// sufficient (checked by the context). +/// +/// - For a list update operation: +/// +/// - Tick energy required for the 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. +/// - Add or remove the account to/from the list. +/// +/// - For a mint operation: +/// +/// - Check that the mint amount is specified with the correct number of decimals. +/// - Tick energy required for the operation. +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows minting. +/// - Mint the amount to the sender, if the resulting circulating supply is +/// within representable range (checked by the context). +/// +/// - For a burn operation: +/// +/// - Check that the burn amount is specified with the correct number of decimals. +/// - Tick energy required for the operation. +/// - Check that the governance account is the sender. +/// - Check that the module is not paused. +/// - Check that the module configuration allows burning. +/// - Burn the amount from the sender, if the sender's balance is +/// sufficient (checked by the context). +/// +/// - For a pause/unpause operation: +/// +/// - Tick energy required for the operation +/// - Check that the governance account is the sender. +/// - Pause/unpause the token. +/// +/// # Arguments +/// +/// - `transaction_execution`: the transaction execution context +/// - `context`: the token context operations interface +/// - `index`: the index of the operation in the transaction, used for error reporting +/// - `operation`: the token operation to execute +pub fn execute_token_update_operation_at_index( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + index: usize, + operation: &TokenOperation, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + match execute_token_update_operation_internal( + transaction_execution, + context, + events, + token.as_mut(), + operation, + ) { + Ok(()) => Ok(()), + Err(err) => { + let token_configuration = token.token_p9_base().token_configuration(context)?; + Err(token_update_error_internal_to_external( + &token_configuration, + index, + operation_name(operation), + err, + )? + .into()) + } + } +} + +/// Translate an internal token update error into the externally visible token +/// update error. +fn token_update_error_internal_to_external( + token_configuration: &TokenConfiguration, + index: usize, + operation_type: &'static str, + err: TokenUpdateErrorInternal, +) -> BlockStateResult { + match err { + TokenUpdateErrorInternal::AccountDoesNotExist(err) => { + Ok(reject::address_not_found(token_configuration, index, err)) + } + TokenUpdateErrorInternal::AmountDecimalsMismatch(err) => Ok( + reject::deserialization_failure_amount_decimals_mismatch(token_configuration, err), + ), + TokenUpdateErrorInternal::InsufficientBalance(err) => Ok(reject::insufficient_balance( + token_configuration, + index, + err, + )), + TokenUpdateErrorInternal::MintWouldOverflow(err) => { + Ok(reject::mint_would_overflow(token_configuration, index, err)) + } + TokenUpdateErrorInternal::OutOfEnergy(_) => Ok(TransactionRejectReason::OutOfEnergy), + TokenUpdateErrorInternal::Paused => Ok(reject::operation_not_permitted_paused( + token_configuration, + index, + operation_type, + )), + TokenUpdateErrorInternal::OperationNotPermitted { + account_address, + reason, + } => Ok(reject::operation_not_permitted( + token_configuration, + index, + account_address, + reason.to_string(), + )), + TokenUpdateErrorInternal::UnsupportedOperation { reason } => { + Ok(reject::unsupported_operation( + token_configuration, + index, + operation_type, + reason.to_string(), + )) + } + TokenUpdateErrorInternal::BlockStateFailure(err) => Err(err), + } +} + +fn operation_name(operation: &TokenOperation) -> &'static str { + match operation { + TokenOperation::Transfer(_) => "transfer", + TokenOperation::Mint(_) => "mint", + TokenOperation::Burn(_) => "burn", + TokenOperation::AddAllowList(_) => "addAllowList", + TokenOperation::RemoveAllowList(_) => "removeAllowList", + TokenOperation::AddDenyList(_) => "addDenyList", + TokenOperation::RemoveDenyList(_) => "removeDenyList", + TokenOperation::Pause(_) => "pause", + TokenOperation::Unpause(_) => "unpause", + TokenOperation::AssignAdminRoles(_) => "assignAdminRoles", + TokenOperation::RevokeAdminRoles(_) => "revokeAdminRoles", + TokenOperation::UpdateMetadata(_) => "updateMetadata", + } +} + +/// Internal variant of `TokenUpdateError` where the reject reason is +/// not encoded as CBOR +#[derive(Debug, thiserror::Error)] +enum TokenUpdateErrorInternal { + #[error("The given account does not exist: {0}")] + AccountDoesNotExist(#[from] AccountNotFoundByAddressError), + #[error("The token amount has wrong number of decimals: {0}")] + AmountDecimalsMismatch(#[from] TokenAmountDecimalsMismatchError), + #[error("Insufficient balance on account: {0}")] + InsufficientBalance(#[from] InsufficientBalanceError), + #[error("Execution out of energy")] + OutOfEnergy(#[from] OutOfEnergyError), + #[error("{0}")] + MintWouldOverflow(#[from] MintWouldOverflowError), + #[error("The token is paused")] + Paused, + #[error("Operation not permitted: {reason}")] + OperationNotPermitted { + account_address: Option, + reason: &'static str, + }, + #[error("Operation not supported: {reason}")] + UnsupportedOperation { reason: &'static str }, + #[error("Block state failure: {0}")] + BlockStateFailure(#[from] BlockStateFailure), +} + +fn execute_token_update_operation_internal( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: TokenPXRefMut<'_>, + token_operation: &TokenOperation, +) -> Result<(), TokenUpdateErrorInternal> { + // Charge energy + let energy_cost = energy_cost(token_operation); + transaction_execution.tick_energy(energy_cost)?; + + const WRONG_PROTOCOL_ERROR: TokenUpdateErrorInternal = + TokenUpdateErrorInternal::UnsupportedOperation { + reason: "Operation not supported at the current protocol version", + }; + + // Execute operation + match token_operation { + TokenOperation::Transfer(transfer) => { + execute_token_transfer(transaction_execution, context, events, token, transfer) + } + TokenOperation::Mint(mint) => { + execute_token_mint(transaction_execution, context, events, token, mint) + } + TokenOperation::Burn(burn) => { + execute_token_burn(transaction_execution, context, events, token, burn) + } + TokenOperation::Pause(_) => { + execute_token_pause(transaction_execution, context, events, token) + } + TokenOperation::Unpause(_) => { + execute_token_unpause(transaction_execution, context, events, token) + } + TokenOperation::AddAllowList(list_operation) => execute_add_allow_list( + transaction_execution, + context, + events, + token, + list_operation, + ), + TokenOperation::RemoveAllowList(list_operation) => execute_remove_allow_list( + transaction_execution, + context, + events, + token, + list_operation, + ), + TokenOperation::AddDenyList(list_operation) => execute_add_deny_list( + transaction_execution, + context, + events, + token, + list_operation, + ), + TokenOperation::RemoveDenyList(list_operation) => execute_remove_deny_list( + transaction_execution, + context, + events, + token, + list_operation, + ), + TokenOperation::AssignAdminRoles(operation) => { + if let TokenPXRefMut::TokenP11(token) = token { + execute_assign_admin_roles(transaction_execution, context, events, token, operation) + } else { + Err(WRONG_PROTOCOL_ERROR) + } + } + TokenOperation::RevokeAdminRoles(operation) => { + if let TokenPXRefMut::TokenP11(token) = token { + execute_revoke_admin_roles(transaction_execution, context, events, token, operation) + } else { + Err(WRONG_PROTOCOL_ERROR) + } + } + TokenOperation::UpdateMetadata(operation) => { + if let TokenPXRefMut::TokenP11(token) = token { + execute_update_metadata(transaction_execution, context, events, token, operation) + } else { + Err(WRONG_PROTOCOL_ERROR) + } + } + } +} + +fn energy_cost(operation: &TokenOperation) -> Energy { + use concordium_base::transactions::cost::*; + + match operation { + TokenOperation::Transfer(_) => PLT_TRANSFER, + TokenOperation::Mint(_) => PLT_MINT, + TokenOperation::Burn(_) => PLT_BURN, + TokenOperation::AddAllowList(_) + | TokenOperation::RemoveAllowList(_) + | TokenOperation::AddDenyList(_) + | TokenOperation::RemoveDenyList(_) => PLT_LIST_UPDATE, + TokenOperation::Pause(_) | TokenOperation::Unpause(_) => PLT_PAUSE, + TokenOperation::AssignAdminRoles(_) | TokenOperation::RevokeAdminRoles(_) => { + PLT_ASSIGN_REVOKE_ROLES + } + TokenOperation::UpdateMetadata(_) => PLT_UPDATE_TOKEN_METADATA, + } +} + +fn check_not_paused( + context: &EntityContext, + token: &TokenP9Base, +) -> Result<(), TokenUpdateErrorInternal> { + if token.is_paused(context) { + return Err(TokenUpdateErrorInternal::Paused); + } + Ok(()) +} + +/// Ensure the sender account from the transaction context is authorized to perform the operation. +fn check_authorized( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + token: TokenPXRef<'_>, + required_role: TokenAdminRole, +) -> Result<(), TokenUpdateErrorInternal> { + if let TokenPXRef::TokenP11(token) = token { + // Ensure the sender holds the specified role. + let account_roles = token.get_account_roles( + context, + transaction_execution.sender_account().account_index(), + )?; + if !account_roles.has(required_role) { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(transaction_execution.sender_account_address()), + reason: "sender is not authorized to perform the operation for this token", + }); + } + } else { + // Ensure the sender is the governance account. + if token + .token_p9_base() + .get_governance_account_index(context)? + != transaction_execution.sender_account().account_index() + { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(transaction_execution.sender_account_address()), + reason: "sender is not the token governance account", + }); + } + } + + Ok(()) +} + +/// Validate that a token transfer between `sender` and `receiver` is permitted +/// by the token module's current state. +/// +/// This checks that the token is not paused, that both accounts satisfy an +/// allow list if one is configured, and that neither account is on a configured +/// deny list. +/// +/// # Errors +/// +/// Returns [`TokenUpdateErrorInternal::Paused`] if the token is paused, or +/// [`TokenUpdateErrorInternal::OperationNotPermitted`] if either account is not +/// permitted to participate in the transfer. +pub fn check_transfer_constraints( + context: &EntityContext, + token: &TokenP9Base, + sender: &Account, + sender_address: AccountAddress, + receiver: &Account, + receiver_address: AccountAddress, + operation_index: usize, +) -> ResultWithBlockStateFailure<(), TransactionRejectReason> { + match check_transfer_constraints_internal( + context, + token, + sender, + sender_address, + receiver, + receiver_address, + ) { + Ok(()) => Ok(()), + Err(err) => { + let token_configuration = token.token_configuration(context)?; + Err(token_update_error_internal_to_external( + &token_configuration, + operation_index, + "transfer", + err, + )? + .into()) + } + } +} + +fn check_transfer_constraints_internal( + context: &EntityContext, + token: &TokenP9Base, + sender: &Account, + sender_address: AccountAddress, + receiver: &Account, + receiver_address: AccountAddress, +) -> Result<(), TokenUpdateErrorInternal> { + check_not_paused(context, token)?; + + if token.has_allow_list(context) { + if !token.get_allow_list_for(context, sender.account_index()) { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(sender_address), + reason: "sender not in allow list", + }); + } + if !token.get_allow_list_for(context, receiver.account_index()) { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(receiver_address), + reason: "recipient not in allow list", + }); + } + } + + if token.has_deny_list(context) { + if token.get_deny_list_for(context, sender.account_index()) { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(sender_address), + reason: "sender in deny list", + }); + } + if token.get_deny_list_for(context, receiver.account_index()) { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(receiver_address), + reason: "recipient in deny list", + }); + } + } + + Ok(()) +} + +fn execute_token_transfer( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: TokenPXRefMut<'_>, + transfer_operation: &TokenTransfer, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + // preprocessing + let raw_amount = + token_amount::to_raw_token_amount(&token_configuration, transfer_operation.amount)?; + + let sender = transaction_execution.sender_account(); + let sender_address = transaction_execution.sender_account_address(); + let receiver_address = transfer_operation.recipient.address; + let receiver = context.account_by_address(&receiver_address)?; + + check_transfer_constraints_internal( + context, + token.token_p9_base(), + sender, + sender_address, + &receiver, + receiver_address, + )?; + + balance_operations::transfer( + context, + events, + token, + sender, + sender_address, + &receiver, + receiver_address, + raw_amount, + transfer_operation.memo.clone().map(Memo::from), + ) + .nest()??; + + Ok(()) +} + +fn execute_token_mint( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + mint_operation: &TokenSupplyUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + // preprocessing + let raw_amount = + token_amount::to_raw_token_amount(&token_configuration, mint_operation.amount)?; + + // operation execution + if !token.token_p9_base().is_mintable(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + }; + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::Mint, + )?; + check_not_paused(context, token.token_p9_base())?; + + balance_operations::mint( + context, + events, + token.token_p9_base_mut(), + transaction_execution.sender_account(), + transaction_execution.sender_account_address(), + raw_amount, + ) + .nest()??; + + Ok(()) +} + +fn execute_token_burn( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: TokenPXRefMut<'_>, + burn_operation: &TokenSupplyUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + // preprocessing + let raw_amount = + token_amount::to_raw_token_amount(&token_configuration, burn_operation.amount)?; + + // operation execution + if !token.token_p9_base().is_burnable(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + } + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::Burn, + )?; + check_not_paused(context, token.token_p9_base())?; + + balance_operations::burn( + context, + events, + token, + transaction_execution.sender_account(), + transaction_execution.sender_account_address(), + raw_amount, + ) + .nest()??; + + Ok(()) +} + +fn execute_token_pause( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::Pause, + )?; + + token.token_p9_base_mut().set_paused(context, true)?; + + let (event_type, details) = TokenModuleEvent::Pause(TokenPauseEventDetails {}).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_token_unpause( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::Pause, + )?; + + token.token_p9_base_mut().set_paused(context, false)?; + + let (event_type, details) = TokenModuleEvent::Unpause(TokenPauseEventDetails {}).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_add_allow_list( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + list_operation: &TokenListUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + if !token.token_p9_base().has_allow_list(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + } + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::UpdateAllowList, + )?; + + let account = context.account_by_address(&list_operation.target.address)?; + account.touch_token_account(context, token.token_p9_base().token_index()); + token + .token_p9_base_mut() + .set_allow_list_for(context, account.account_index(), true)?; + + let event_details = TokenListUpdateEventDetails { + target: list_operation.target.clone(), + }; + let (event_type, details) = TokenModuleEvent::AddAllowList(event_details).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_add_deny_list( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + list_operation: &TokenListUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + if !token.token_p9_base().has_deny_list(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + } + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::UpdateDenyList, + )?; + + let account = context.account_by_address(&list_operation.target.address)?; + account.touch_token_account(context, token.token_p9_base().token_index()); + token + .token_p9_base_mut() + .set_deny_list_for(context, account.account_index(), true)?; + + let event_details = TokenListUpdateEventDetails { + target: list_operation.target.clone(), + }; + let (event_type, details) = TokenModuleEvent::AddDenyList(event_details).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_remove_allow_list( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + list_operation: &TokenListUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + if !token.token_p9_base().has_allow_list(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + } + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::UpdateAllowList, + )?; + + let account = context.account_by_address(&list_operation.target.address)?; + account.touch_token_account(context, token.token_p9_base().token_index()); + token + .token_p9_base_mut() + .set_allow_list_for(context, account.account_index(), false)?; + + let event_details = TokenListUpdateEventDetails { + target: list_operation.target.clone(), + }; + let (event_type, details) = TokenModuleEvent::RemoveAllowList(event_details).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_remove_deny_list( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + mut token: TokenPXRefMut<'_>, + list_operation: &TokenListUpdateDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base().token_configuration(context)?; + + if !token.token_p9_base().has_deny_list(context) { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature not enabled", + }); + } + check_authorized( + transaction_execution, + context, + token.as_ref(), + TokenAdminRole::UpdateDenyList, + )?; + + let account = context.account_by_address(&list_operation.target.address)?; + account.touch_token_account(context, token.token_p9_base().token_index()); + token + .token_p9_base_mut() + .set_deny_list_for(context, account.account_index(), false)?; + + let event_details = TokenListUpdateEventDetails { + target: list_operation.target.clone(), + }; + let (event_type, details) = TokenModuleEvent::RemoveDenyList(event_details).encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn check_roles_supported( + context: &mut EntityContext, + token: &TokenP11, + roles: &[TokenAdminRole], +) -> Result<(), TokenUpdateErrorInternal> { + for role in roles { + let supported = match role { + TokenAdminRole::UpdateAdminRoles => true, + TokenAdminRole::Mint => token.token_p9_base.is_mintable(context), + TokenAdminRole::Burn => token.token_p9_base.is_burnable(context), + TokenAdminRole::UpdateAllowList => token.token_p9_base.has_allow_list(context), + TokenAdminRole::UpdateDenyList => token.token_p9_base.has_deny_list(context), + TokenAdminRole::Pause => true, + TokenAdminRole::UpdateMetadata => true, + }; + if !supported { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "feature using role is not enabled", + }); + } + } + Ok(()) +} + +fn execute_assign_admin_roles( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + operation: &TokenUpdateAdminRolesDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base.token_configuration(context)?; + + check_authorized( + transaction_execution, + context, + TokenPXRef::TokenP11(token), + TokenAdminRole::UpdateAdminRoles, + )?; + check_roles_supported(context, token, &operation.roles)?; + + let account = context.account_by_address(&operation.account.address)?; + token.assign_account_roles(context, account.account_index(), &operation.roles)?; + + let event = TokenModuleEvent::AssignAdminRoles(TokenUpdateAdminRolesEventDetails { + roles: operation.roles.clone(), + account: operation.account.clone(), + }); + let (event_type, details) = event.encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_revoke_admin_roles( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + operation: &TokenUpdateAdminRolesDetails, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base.token_configuration(context)?; + + check_authorized( + transaction_execution, + context, + TokenPXRef::TokenP11(token), + TokenAdminRole::UpdateAdminRoles, + )?; + check_roles_supported(context, token, &operation.roles)?; + + let account = context.account_by_address(&operation.account.address)?; + if account.account_index() == transaction_execution.sender_account().account_index() + && operation.roles.contains(&TokenAdminRole::UpdateAdminRoles) + { + return Err(TokenUpdateErrorInternal::OperationNotPermitted { + account_address: Some(operation.account.address), + reason: "Sender not allowed to remove own update-admin-role role", + }); + } + token.revoke_account_roles(context, account.account_index(), &operation.roles)?; + + let event = TokenModuleEvent::RevokeAdminRoles(TokenUpdateAdminRolesEventDetails { + roles: operation.roles.clone(), + account: operation.account.clone(), + }); + let (event_type, details) = event.encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} + +fn execute_update_metadata( + transaction_execution: &mut TransactionExecution, + context: &mut EntityContext, + events: &mut impl Extend, + token: &mut TokenP11, + metadata_url: &MetadataUrl, +) -> Result<(), TokenUpdateErrorInternal> { + let token_configuration = token.token_p9_base.token_configuration(context)?; + + if !metadata_url.additional.is_empty() { + return Err(TokenUpdateErrorInternal::UnsupportedOperation { + reason: "Unknown additional metadata fields", + }); + } + check_authorized( + transaction_execution, + context, + TokenPXRef::TokenP11(token), + TokenAdminRole::UpdateMetadata, + )?; + token + .token_p9_base + .set_metadata_url(context, metadata_url)?; + let event = TokenModuleEvent::UpdateMetadata(TokenUpdateMetadataEventDetails { + metadata_url: metadata_url.clone(), + }); + let (event_type, details) = event.encode_event(); + events.extend(Some(BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: token_configuration.token_id, + event_type: event_type.to_type_discriminator(), + details, + }))); + + Ok(()) +} diff --git a/plt/plt-scheduler/src/scheduler.rs b/plt/plt-scheduler/src/scheduler.rs new file mode 100644 index 0000000000..c93cf9547a --- /dev/null +++ b/plt/plt-scheduler/src/scheduler.rs @@ -0,0 +1,31 @@ +//! Entry points to calling the scheduler. The scheduler is responsible for executing +//! transaction and update instruction payloads. + +use plt_block_state::failure::BlockStateFailure; + +pub mod p11; +pub mod p9; + +/// Unrecoverable error executing transaction. This represents the +/// return value of [`execute_transaction`] for transactions that cannot +/// be correctly executed. +#[derive(Debug, thiserror::Error)] +pub enum TransactionExecutionError { + #[error("Unexpected transaction payload that cannot be handled")] + UnexpectedPayload, + /// Error in the block state. This is generally an error that should never happen and is unrecoverable. + #[error("Block state failure: {0}")] + BlockStateFailure(#[from] BlockStateFailure), +} + +/// Unrecoverable error executing chain update. This represents the +/// return value of [`execute_chain_update`] for chain updates that cannot +/// be correctly executed. +#[derive(Debug, thiserror::Error)] +pub enum ChainUpdateExecutionError { + #[error("Unexpected chain update payload that cannot be handled")] + UnexpectedPayload, + /// Error in the block state. This is generally an error that should never happen and is unrecoverable. + #[error("Block state failure: {0}")] + BlockStateFailure(#[from] BlockStateFailure), +} diff --git a/plt/plt-scheduler/src/scheduler/p11.rs b/plt/plt-scheduler/src/scheduler/p11.rs new file mode 100644 index 0000000000..3b3f598c8e --- /dev/null +++ b/plt/plt-scheduler/src/scheduler/p11.rs @@ -0,0 +1,564 @@ +use crate::failure::ResultWithBlockStateFailureExt; +use crate::scheduler::{ChainUpdateExecutionError, TransactionExecutionError}; +use crate::transaction_execution::{OutOfEnergyError, TransactionExecution}; +use crate::{TransactionContext, protocol_level_locks, protocol_level_tokens}; +use concordium_base::protocol_level_tokens::meta_operations::{ + LockOperation, MetaUpdateOperation, MetaUpdateOperations, MetaUpdatePayload, +}; +use concordium_base::protocol_level_tokens::{TokenId, TokenOperation}; +use concordium_base::transactions; +use concordium_base::transactions::Payload; +use concordium_base::updates::UpdatePayload; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; +use plt_block_state::persistent::chain_parameters::p11::PersistentChainParametersP11; +use plt_block_state::utils; +use plt_scheduler_types::types::execution::{ + ChainUpdateOutcome, TransactionExecutionSummary, TransactionOutcome, +}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; + +/// Execute a transaction payload modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a reject reason. Additionally, the +/// amount of energy used by the execution is returned. The returned values are represented +/// via the type [`TransactionExecutionSummary`]. +/// +/// NOTICE: The caller must ensure to rollback state changes in case of the transaction being rejected. +/// +/// # Arguments +/// +/// - `sender_account` The account initiating the transaction (signer of the transaction) +/// - `transaction_context` Transacstion context containing sender, energy limit etc. +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The transaction payload to execute +/// +/// # Errors +/// +/// - [`TransactionExecutionError`] If executing the transaction fails with an unrecoverable error. +/// Returning this error will terminate the scheduler. +pub fn execute_transaction( + context: &mut EntityContext, + block_state: &mut BlockStateP11, + transaction_context: TransactionContext, + sender_account: Account, + payload: Payload, + chain_parameters: &PersistentChainParametersP11, +) -> Result { + let mut execution = TransactionExecution::new(transaction_context, sender_account); + + let outcome = match payload { + Payload::TokenUpdate { payload } => { + protocol_level_tokens::p11::execute_token_update_transaction( + context, + &mut execution, + block_state, + payload, + )? + } + Payload::MetaUpdate { payload } => execute_meta_update_transaction( + context, + &mut execution, + block_state, + payload, + chain_parameters, + )?, + _ => return Err(TransactionExecutionError::UnexpectedPayload), + }; + + Ok(TransactionExecutionSummary { + outcome, + energy_used: execution.energy_used(), + }) +} + +/// Execute [`MetaUpdatePayload`] +fn execute_meta_update_transaction( + context: &mut EntityContext, + transaction_execution: &mut TransactionExecution, + block_state: &mut BlockStateP11, + payload: MetaUpdatePayload, + chain_parameters: &PersistentChainParametersP11, +) -> BlockStateResult { + // Charge energy + if let Err(err) = + transaction_execution.tick_energy(transactions::cost::META_UPDATE_TRANSACTIONS) + { + let _: OutOfEnergyError = err; // assert type of error + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::OutOfEnergy, + )); + } + + let mut events = Vec::new(); + + // Decode operations + let operations: Vec = + match utils::cbor_decode::(payload.operations) { + Ok(payload) => payload.operations, + Err(_) => { + return Ok(TransactionOutcome::Rejected( + TransactionRejectReason::SerializationFailure, + )); + } + }; + + // Execute operations + for (index, operation) in operations.into_iter().enumerate() { + match MetaUpdateOperationKind::from(operation) { + MetaUpdateOperationKind::Token(token_id, token_operation) => { + match protocol_level_tokens::p11::execute_token_update_operation( + context, + transaction_execution, + block_state, + index, + &token_id, + token_operation, + &mut events, + ) + .nest()? + { + Ok(()) => (), + Err(reject_reason) => { + return Ok(TransactionOutcome::Rejected(reject_reason)); + } + } + } + MetaUpdateOperationKind::Lock(lock_operation) => { + match protocol_level_locks::p11::execute_lock_operation( + context, + transaction_execution, + block_state, + chain_parameters.max_lock_duration, + index, + lock_operation, + &mut events, + ) + .nest()? + { + Ok(()) => (), + Err(reject_reason) => { + return Ok(TransactionOutcome::Rejected(reject_reason)); + } + } + } + } + } + + // Return events + Ok(TransactionOutcome::Success(events)) +} + +/// Execute a chain update modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a failure kind. +/// +/// NOTICE: The caller must ensure to rollback state changes in case a failure kind is returned. +/// +/// # Arguments +/// +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The chain update payload to execute +/// +/// # Errors +/// +/// - [`ChainUpdateExecutionError`] If executing the chain update failed in an unrecoverable way. +/// Returning this error will terminate the scheduler. +pub fn execute_chain_update( + context: &mut EntityContext, + block_state: &mut BlockStateP11, + payload: UpdatePayload, +) -> Result { + match payload { + UpdatePayload::CreatePlt(create_plt) => { + Ok(protocol_level_tokens::p11::execute_create_plt_chain_update( + context, + block_state, + create_plt, + )?) + } + _ => Err(ChainUpdateExecutionError::UnexpectedPayload), + } +} + +/// Execute a chain update modifying P11 external chain parameters. +/// +/// # Arguments +/// +/// - `chain_parameters` External chain parameters to update. +/// - `payload` The chain update payload to execute. +/// +/// # Errors +/// +/// Returns [`ChainUpdateExecutionError::UnexpectedPayload`] if the payload is +/// not a P11 external chain-parameter update. +pub fn execute_chain_parameters_update( + chain_parameters: &mut PersistentChainParametersP11, + payload: UpdatePayload, +) -> Result<(), ChainUpdateExecutionError> { + match payload { + UpdatePayload::MaxLockDuration(duration) => { + chain_parameters.max_lock_duration = duration; + Ok(()) + } + _ => Err(ChainUpdateExecutionError::UnexpectedPayload), + } +} + +/// A discriminated version of [`MetaUpdateOperation`] for the purpose of +/// dispatching to the appropriate operation handler. +#[derive(PartialEq, Debug, Clone)] +enum MetaUpdateOperationKind { + /// A [`TokenOperation`] for a specific [`TokenId`]. + Token(TokenId, TokenOperation), + /// A [`LockOperation`]. + Lock(LockOperation), +} + +impl From for MetaUpdateOperationKind { + fn from(value: MetaUpdateOperation) -> Self { + match value { + MetaUpdateOperation::Transfer(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::Transfer(details)) + } + MetaUpdateOperation::Mint(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::Mint(details)) + } + MetaUpdateOperation::Burn(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::Burn(details)) + } + MetaUpdateOperation::AddAllowList(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::AddAllowList(details)) + } + MetaUpdateOperation::RemoveAllowList(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::RemoveAllowList(details)) + } + MetaUpdateOperation::AddDenyList(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::AddDenyList(details)) + } + MetaUpdateOperation::RemoveDenyList(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::RemoveDenyList(details)) + } + MetaUpdateOperation::Pause(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::Pause(details)) + } + MetaUpdateOperation::Unpause(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::Unpause(details)) + } + MetaUpdateOperation::AssignAdminRoles(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::AssignAdminRoles(details)) + } + MetaUpdateOperation::RevokeAdminRoles(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::RevokeAdminRoles(details)) + } + MetaUpdateOperation::UpdateMetadata(details) => { + let (token_id, details) = details.into(); + Self::Token(token_id, TokenOperation::UpdateMetadata(details)) + } + MetaUpdateOperation::LockFund(details) => Self::Lock(LockOperation::Fund(details)), + MetaUpdateOperation::LockSend(details) => Self::Lock(LockOperation::Send(details)), + MetaUpdateOperation::LockReturn(details) => Self::Lock(LockOperation::Return(details)), + MetaUpdateOperation::LockCreate(details) => Self::Lock(LockOperation::Create(details)), + MetaUpdateOperation::LockCancel(details) => Self::Lock(LockOperation::Cancel(details)), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use concordium_base::common; + use concordium_base::contracts_common::Duration; + use concordium_base::transactions::Memo; + + #[test] + fn execute_max_lock_duration_update() { + let mut chain_parameters = PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(42), + }; + execute_chain_parameters_update( + &mut chain_parameters, + UpdatePayload::MaxLockDuration(Duration::from_millis(123)), + ) + .expect("max lock duration update should succeed"); + + assert_eq!( + chain_parameters.max_lock_duration, + Duration::from_millis(123) + ); + } + + #[test] + fn test_meta_operation_token_operation_conversion() { + use concordium_base::protocol_level_tokens::meta_operations::*; + use concordium_base::protocol_level_tokens::*; + // For each meta-update operation variant: + // - construct a meta-update operation with some test data + // - construct the corresponding token operation with the same test data + // - convert in each direction and check that the result matches the original + // - construct the meta-update operation using the `meta_operations` helper function + // and check that it matches the original + let token_id: TokenId = "tokenid1".parse().unwrap(); + let amount = TokenAmount::from_raw(100000, 2); + const ADDRESS: common::types::AccountAddress = common::types::AccountAddress([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, + ]); + let account = CborHolderAccount::from(ADDRESS); + let cbor_memo = CborMemo::Raw(Memo::try_from(vec![1, 2, 3, 4]).unwrap()); + let memo = Some(cbor_memo.clone()); + + let token_transfer = TokenOperation::Transfer(TokenTransfer { + amount, + recipient: account.clone(), + memo: memo.clone(), + }); + let meta_transfer = MetaUpdateOperation::Transfer(MetaTokenTransfer { + token: token_id.clone(), + amount, + recipient: account.clone(), + memo: memo.clone(), + }); + assert_eq!( + transfer_tokens_with_memo(token_id.clone(), ADDRESS, amount, cbor_memo.clone()), + meta_transfer + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_transfer.clone())), + meta_transfer + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_transfer), + meta_transfer.into(), + ); + + let token_mint = TokenOperation::Mint(TokenSupplyUpdateDetails { amount }); + let meta_mint = MetaUpdateOperation::Mint(MetaTokenSupplyUpdateDetails { + token: token_id.clone(), + amount, + }); + assert_eq!(mint_tokens(token_id.clone(), amount), meta_mint); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_mint.clone())), + meta_mint + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_mint), + meta_mint.into(), + ); + + let token_burn = TokenOperation::Burn(TokenSupplyUpdateDetails { amount }); + let meta_burn = MetaUpdateOperation::Burn(MetaTokenSupplyUpdateDetails { + token: token_id.clone(), + amount, + }); + assert_eq!(burn_tokens(token_id.clone(), amount), meta_burn); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_burn.clone())), + meta_burn + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_burn), + meta_burn.into(), + ); + + let token_add_allow_list = TokenOperation::AddAllowList(TokenListUpdateDetails { + target: account.clone(), + }); + let meta_add_allow_list = MetaUpdateOperation::AddAllowList(MetaTokenListUpdateDetails { + token: token_id.clone(), + target: account.clone(), + }); + assert_eq!( + add_token_allow_list(token_id.clone(), ADDRESS), + meta_add_allow_list + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_add_allow_list.clone())), + meta_add_allow_list + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_add_allow_list), + meta_add_allow_list.into(), + ); + + let token_remove_allow_list = TokenOperation::RemoveAllowList(TokenListUpdateDetails { + target: account.clone(), + }); + let meta_remove_allow_list = + MetaUpdateOperation::RemoveAllowList(MetaTokenListUpdateDetails { + token: token_id.clone(), + target: account.clone(), + }); + assert_eq!( + remove_token_allow_list(token_id.clone(), ADDRESS), + meta_remove_allow_list + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_remove_allow_list.clone())), + meta_remove_allow_list + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_remove_allow_list), + meta_remove_allow_list.into(), + ); + + let token_add_deny_list = TokenOperation::AddDenyList(TokenListUpdateDetails { + target: account.clone(), + }); + let meta_add_deny_list = MetaUpdateOperation::AddDenyList(MetaTokenListUpdateDetails { + token: token_id.clone(), + target: account.clone(), + }); + assert_eq!( + add_token_deny_list(token_id.clone(), ADDRESS), + meta_add_deny_list + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_add_deny_list.clone())), + meta_add_deny_list + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_add_deny_list), + meta_add_deny_list.into(), + ); + + let token_remove_deny_list = TokenOperation::RemoveDenyList(TokenListUpdateDetails { + target: account.clone(), + }); + let meta_remove_deny_list = + MetaUpdateOperation::RemoveDenyList(MetaTokenListUpdateDetails { + token: token_id.clone(), + target: account.clone(), + }); + assert_eq!( + remove_token_deny_list(token_id.clone(), ADDRESS), + meta_remove_deny_list + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_remove_deny_list.clone())), + meta_remove_deny_list + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_remove_deny_list), + meta_remove_deny_list.into(), + ); + + let token_pause = TokenOperation::Pause(TokenPauseDetails {}); + let meta_pause = MetaUpdateOperation::Pause(MetaTokenPauseDetails { + token: token_id.clone(), + }); + assert_eq!(pause(token_id.clone()), meta_pause); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_pause.clone())), + meta_pause + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_pause), + meta_pause.into(), + ); + + let token_unpause = TokenOperation::Unpause(TokenPauseDetails {}); + let meta_unpause = MetaUpdateOperation::Unpause(MetaTokenPauseDetails { + token: token_id.clone(), + }); + assert_eq!(unpause(token_id.clone()), meta_unpause); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_unpause.clone())), + meta_unpause + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_unpause), + meta_unpause.into(), + ); + + let assign_roles = vec![TokenAdminRole::Mint, TokenAdminRole::Pause]; + let token_assign_admin_roles = + TokenOperation::AssignAdminRoles(TokenUpdateAdminRolesDetails { + roles: assign_roles.clone(), + account: account.clone(), + }); + let meta_assign_admin_roles = + MetaUpdateOperation::AssignAdminRoles(MetaTokenUpdateAdminRolesDetails { + token: token_id.clone(), + roles: assign_roles.clone(), + account: account.clone(), + }); + assert_eq!( + assign_admin_roles(token_id.clone(), ADDRESS, assign_roles.clone()), + meta_assign_admin_roles + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_assign_admin_roles.clone())), + meta_assign_admin_roles + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_assign_admin_roles), + meta_assign_admin_roles.into(), + ); + + let revoke_roles = vec![ + TokenAdminRole::Burn, + TokenAdminRole::UpdateMetadata, + TokenAdminRole::UpdateDenyList, + ]; + let token_revoke_admin_roles = + TokenOperation::RevokeAdminRoles(TokenUpdateAdminRolesDetails { + roles: revoke_roles.clone(), + account: account.clone(), + }); + let meta_revoke_admin_roles = + MetaUpdateOperation::RevokeAdminRoles(MetaTokenUpdateAdminRolesDetails { + token: token_id.clone(), + roles: revoke_roles.clone(), + account: account.clone(), + }); + assert_eq!( + revoke_admin_roles(token_id.clone(), ADDRESS, revoke_roles.clone()), + meta_revoke_admin_roles + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_revoke_admin_roles.clone())), + meta_revoke_admin_roles + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_revoke_admin_roles), + meta_revoke_admin_roles.into(), + ); + + let metadata_url = MetadataUrl { + url: "https://example.com/metadata.json".to_string(), + checksum_sha_256: Some([0u8; 32].into()), + additional: Default::default(), + }; + let token_update_metadata = TokenOperation::UpdateMetadata(metadata_url.clone()); + let meta_update_metadata = MetaUpdateOperation::UpdateMetadata(MetaMetadataUrlDetails { + token: token_id.clone(), + metadata_url: metadata_url.clone(), + }); + assert_eq!( + update_metadata(token_id.clone(), metadata_url.clone()), + meta_update_metadata + ); + assert_eq!( + MetaUpdateOperation::from((token_id.clone(), token_update_metadata.clone())), + meta_update_metadata + ); + assert_eq!( + MetaUpdateOperationKind::Token(token_id.clone(), token_update_metadata), + meta_update_metadata.into(), + ); + } +} diff --git a/plt/plt-scheduler/src/scheduler/p9.rs b/plt/plt-scheduler/src/scheduler/p9.rs new file mode 100644 index 0000000000..971ba92844 --- /dev/null +++ b/plt/plt-scheduler/src/scheduler/p9.rs @@ -0,0 +1,85 @@ +use crate::scheduler::{ChainUpdateExecutionError, TransactionExecutionError}; +use crate::transaction_execution::TransactionExecution; +use crate::{TransactionContext, protocol_level_tokens}; +use concordium_base::transactions::Payload; +use concordium_base::updates::UpdatePayload; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, TransactionExecutionSummary}; + +/// Execute a transaction payload modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a reject reason. Additionally, the +/// amount of energy used by the execution is returned. The returned values are represented +/// via the type [`TransactionExecutionSummary`]. +/// +/// NOTICE: The caller must ensure to rollback state changes in case of the transaction being rejected. +/// +/// # Arguments +/// +/// - `sender_account` The account initiating the transaction (signer of the transaction) +/// - `transaction_context` Transacstion context containing sender, energy limit etc. +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The transaction payload to execute +/// +/// # Errors +/// +/// - [`TransactionExecutionError`] If executing the transaction fails with an unrecoverable error. +/// Returning this error will terminate the scheduler. +pub fn execute_transaction( + context: &mut EntityContext, + block_state: &mut BlockStateP9, + transaction_context: TransactionContext, + sender_account: Account, + payload: Payload, +) -> Result { + let mut execution = TransactionExecution::new(transaction_context, sender_account); + + let outcome = match payload { + Payload::TokenUpdate { payload } => { + protocol_level_tokens::p9::execute_token_update_transaction( + context, + &mut execution, + block_state, + payload, + )? + } + _ => return Err(TransactionExecutionError::UnexpectedPayload), + }; + + Ok(TransactionExecutionSummary { + outcome, + energy_used: execution.energy_used(), + }) +} + +/// Execute a chain update modifying `block_state` accordingly. +/// Returns the events produced if successful, otherwise a failure kind. +/// +/// NOTICE: The caller must ensure to rollback state changes in case a failure kind is returned. +/// +/// # Arguments +/// +/// - `block_state` Block state that can be queried and updated during execution. +/// - `payload` The chain update payload to execute +/// +/// # Errors +/// +/// - [`ChainUpdateExecutionError`] If executing the chain update failed in an unrecoverable way. +/// Returning this error will terminate the scheduler. +pub fn execute_chain_update( + context: &mut EntityContext, + block_state: &mut BlockStateP9, + payload: UpdatePayload, +) -> Result { + match payload { + UpdatePayload::CreatePlt(create_plt) => { + Ok(protocol_level_tokens::p9::execute_create_plt_chain_update( + context, + block_state, + create_plt, + )?) + } + _ => Err(ChainUpdateExecutionError::UnexpectedPayload), + } +} diff --git a/plt/plt-scheduler/src/transaction_execution.rs b/plt/plt-scheduler/src/transaction_execution.rs new file mode 100644 index 0000000000..6836c5a2d7 --- /dev/null +++ b/plt/plt-scheduler/src/transaction_execution.rs @@ -0,0 +1,119 @@ +//! Context for transaction execution. + +use concordium_base::base::{Energy, Nonce}; +use concordium_base::contracts_common::{AccountAddress, Timestamp}; +use plt_block_state::entity::accounts::Account; + +/// Transaction execution ran out of energy. +#[derive(Debug, thiserror::Error)] +#[error("Execution out of energy")] +pub struct OutOfEnergyError; + +#[derive(Debug, Clone)] +pub struct TransactionContext { + /// Limit for how much energy the execution can use. An [`OutOfEnergyError`] error is + /// returned if the limit is reached. + pub energy_limit: Energy, + /// The address of the account which signed as the sender of the transaction. This need not be + /// the canonical address of the account, it can be an account alias. + pub sender_account_address: AccountAddress, + /// The sequence number of the transaction as specified in the transaction header. + pub transaction_sequence_number: Nonce, + /// Timestamp of the block in which the transaction is executed. + pub block_timestamp: Timestamp, +} + +/// Tracks the energy remaining and some context during the execution. +pub struct TransactionExecution { + /// Limit for how much energy the execution can use. An [`OutOfEnergyError`] error is + /// returned if the limit is reached. + energy_limit: Energy, + /// Energy used so far by execution. Energy is always charged in advance for each step executed. + energy_used: Energy, + /// The account which signed as the sender of the transaction. + sender_account: Account, + /// The address of the account which signed as the sender of the transaction. This need not be + /// the canonical address of the account, it can be an account alias. + sender_account_address: AccountAddress, + /// The sequence number of the transaction as specified in the transaction header. + transaction_sequence_number: Nonce, + /// Timestamp of the block in which the transaction is executed. + block_timestamp: Timestamp, + /// The number of locks that have been created during the execution of the transaction so far. + /// This is used to generate unique lock IDs for locks created during execution. + locks_created: u64, +} + +impl TransactionExecution { + /// Construct new transaction execution context. + pub fn new(transaction_context: TransactionContext, sender_account: Account) -> Self { + Self { + energy_used: 0.into(), + energy_limit: transaction_context.energy_limit, + sender_account, + sender_account_address: transaction_context.sender_account_address, + transaction_sequence_number: transaction_context.transaction_sequence_number, + block_timestamp: transaction_context.block_timestamp, + locks_created: 0, + } + } + + /// The account initiating the transaction. + pub fn sender_account(&self) -> &Account { + &self.sender_account + } + + /// The account address of the account initiating the transaction. This need + /// not be canonical address of the account, it can be an alias. + pub fn sender_account_address(&self) -> AccountAddress { + self.sender_account_address + } + + /// Energy used so far by execution. + pub fn energy_used(&self) -> Energy { + self.energy_used + } + + /// 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. + pub fn tick_energy(&mut self, energy: Energy) -> Result<(), OutOfEnergyError> { + // self.energy_limit - self.energy_used should never underflow, but we safeguard with checked_sub + if self + .energy_limit + .checked_sub(self.energy_used) + .ok_or(OutOfEnergyError)? + >= energy + { + self.energy_used = self.energy_used + energy; + Ok(()) + } else { + // Charge all available energy in case of limit is reached + self.energy_used = self.energy_limit; + Err(OutOfEnergyError) + } + } + + /// The sequence number of the transaction as specified in the transaction header. + pub fn transaction_sequence_number(&self) -> Nonce { + self.transaction_sequence_number + } + + /// The timestamp of the block in which the transaction is executed. + pub fn timestamp(&self) -> Timestamp { + self.block_timestamp + } + + /// Get the next lock creation order number and increment the counter. + pub fn next_lock_creation_order(&mut self) -> u64 { + let creation_order = self.locks_created; + self.locks_created += 1; + creation_order + } +} diff --git a/plt/plt-scheduler/tests/lock_cancel.rs b/plt/plt-scheduler/tests/lock_cancel.rs new file mode 100644 index 0000000000..7a1928a608 --- /dev/null +++ b/plt/plt-scheduler/tests/lock_cancel.rs @@ -0,0 +1,470 @@ +//! Tests for cancelling a PLT lock. + +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use assert_matches::assert_matches; +use concordium_base::protocol_level_tokens::CborMemo; +use concordium_base::protocol_level_tokens::meta_operations::lock_fund; +use concordium_base::{ + base::Energy, + common::cbor, + protocol_level_locks::{LockControllerSimpleV0Capability, LockId}, + protocol_level_tokens::{ + CborHolderAccount, RawCbor, TokenId, TokenListUpdateDetails, TokenOperation, + meta_operations::{MetaUpdatePayload, lock_cancel}, + }, + transactions::Payload, +}; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::block_state::LockNotFoundByIdError; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::TokenHolder; +use plt_scheduler_types::types::{ + events::{BlockItemEvent, LockDestroyEvent}, + execution::TransactionOutcome, + tokens::{RawTokenAmount, TokenAmount}, +}; + +mod utils; + +/// Test cancelling a lock by an authorized canceller before the lock's expiry time. +#[test] +fn test_cancel_by_canceller() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_index_2 = context.external.create_account().account_index(); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenInitTestParams::default().mintable().burnable(); + let (_gov_acct, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + plt_x.clone(), + parameters, + 2, + Some(RawTokenAmount::from(10000)), + ); + + let lock_id = LockId { + account_index: account_index_1.into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![account_index_1], + grants: vec![LockControllerSimpleV0Grant { + account: account_index_2, + roles: vec![LockControllerSimpleV0Capability::Cancel], + }], + tokens: vec![plt_x.clone()], + expiry: 1000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let transaction_context = plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: context.external.account_canonical_address(account_index_2), + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }; + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_cancel(lock_id.clone(), None)])), + }, + }; + let summary = block_state + .execute_transaction(&mut context, transaction_context, account_index_2, payload) + .unwrap(); + assert_matches!(summary.outcome, TransactionOutcome::Success(events) => { + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::LockDestroyed(LockDestroyEvent{lock_id: event_lock_id}) => { + assert_eq!(event_lock_id, &lock_id); + }) + }); +} + +/// Test cancelling a lock by an unauthorized account before the lock's expiry time. +#[test] +fn test_cancel_unauthorized() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_index_2 = context.external.create_account().account_index(); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenInitTestParams::default().mintable().burnable(); + let (_gov_acct, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + plt_x.clone(), + parameters, + 2, + Some(RawTokenAmount::from(10000)), + ); + + let lock_id = LockId { + account_index: account_index_1.into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![account_index_1], + grants: vec![LockControllerSimpleV0Grant { + account: account_index_1, + roles: vec![LockControllerSimpleV0Capability::Cancel], + }], + tokens: vec![plt_x.clone()], + expiry: 1000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let sender_addr = context.external.account_canonical_address(account_index_2); + let transaction_context = plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }; + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_cancel(lock_id.clone(), None)])), + }, + }; + let summary = block_state + .execute_transaction(&mut context, transaction_context, account_index_2, payload) + .unwrap(); + assert_matches!(summary.outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockCancelNotAuthorized(lock_id.clone(), sender_addr)); + }); +} + +/// Test cancelling a lock after the lock's expiry time, by an account with no +/// cancel capability. +#[test] +fn test_cancel_after_expiry() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_index_2 = context.external.create_account().account_index(); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenInitTestParams::default().mintable().burnable(); + let (_gov_acct, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + plt_x.clone(), + parameters, + 2, + Some(RawTokenAmount::from(10000)), + ); + + let lock_id = LockId { + account_index: account_index_1.into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![account_index_1], + grants: vec![LockControllerSimpleV0Grant { + account: account_index_2, + roles: vec![LockControllerSimpleV0Capability::Cancel], + }], + tokens: vec![plt_x.clone()], + expiry: 1000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let transaction_context = plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: context.external.account_canonical_address(account_index_1), + transaction_sequence_number: 1.into(), + block_timestamp: 1000001.into(), + }; + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_cancel(lock_id.clone(), None)])), + }, + }; + let summary = block_state + .execute_transaction(&mut context, transaction_context, account_index_1, payload) + .unwrap(); + assert_matches!(summary.outcome, TransactionOutcome::Success(events) => { + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::LockDestroyed(LockDestroyEvent{lock_id: event_lock_id}) => { + assert_eq!(event_lock_id, &lock_id); + }) + }); +} + +/// Test cancelling a lock with balances. +#[test] +fn test_cancel_with_balances() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_index_2 = context.external.create_account().account_index(); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenInitTestParams::default().mintable().burnable(); + let (plt_x_gov_acct, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + plt_x.clone(), + parameters, + 2, + Some(RawTokenAmount::from(10000)), + ); + let plt_x_gov_acct_address = context + .account_by_index(plt_x_gov_acct.account_index()) + .unwrap() + .canonical_account_address; + let plt_y: TokenId = "pltY".parse().unwrap(); + let (plt_y_gov_acct, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + plt_y.clone(), + TokenInitTestParams::default(), + 6, + Some(RawTokenAmount::from(10000000)), + ); + let plt_y_gov_acct_address = context + .account_by_index(plt_y_gov_acct.account_index()) + .unwrap() + .canonical_account_address; + + let lock_id = LockId { + account_index: account_index_1.into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![account_index_1], + grants: vec![ + LockControllerSimpleV0Grant { + account: account_index_2, + roles: vec![LockControllerSimpleV0Capability::Cancel], + }, + LockControllerSimpleV0Grant { + account: plt_x_gov_acct.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }, + LockControllerSimpleV0Grant { + account: plt_y_gov_acct.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }, + ], + tokens: vec![plt_x.clone()], + expiry: 1000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + plt_x_gov_acct.account_index(), + &plt_x.clone(), + RawTokenAmount::from(500), + ); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + plt_y_gov_acct.account_index(), + &plt_y.clone(), + RawTokenAmount::from(1000), + ); + + let transaction_context = plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: context.external.account_canonical_address(account_index_2), + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }; + let memo = CborMemo::Raw(vec![1u8, 2, 3].try_into().unwrap()); + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_cancel( + lock_id.clone(), + Some(memo.clone()), + )])), + }, + }; + let summary = block_state + .execute_transaction(&mut context, transaction_context, account_index_2, payload) + .unwrap(); + assert_matches!(summary.outcome, TransactionOutcome::Success(events) => { + assert_eq!(events.len(), 3); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, plt_x); + assert_eq!(transfer.amount, TokenAmount::from_raw(500, 2)); + assert_eq!(transfer.from, TokenHolder::Account(plt_x_gov_acct_address)); + assert_eq!(transfer.to, TokenHolder::Account(plt_x_gov_acct_address)); + assert_eq!(transfer.from_lock.as_ref(), Some(&lock_id)); + assert_eq!(transfer.to_lock, None); + assert_eq!(transfer.memo, Some(memo.clone().into())); + }); + assert_matches!(&events[1], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, plt_y); + assert_eq!(transfer.amount, TokenAmount::from_raw(1000, 6)); + assert_eq!(transfer.from, TokenHolder::Account(plt_y_gov_acct_address)); + assert_eq!(transfer.to, TokenHolder::Account(plt_y_gov_acct_address)); + assert_eq!(transfer.from_lock.as_ref(), Some(&lock_id)); + assert_eq!(transfer.to_lock, None); + assert_eq!(transfer.memo, Some(memo.into())); + }); + assert_matches!(&events[2], BlockItemEvent::LockDestroyed(LockDestroyEvent{lock_id: event_lock_id}) => { + assert_eq!(event_lock_id, &lock_id); + }) + }); + assert_matches!(block_state.lock_by_id(&context, &lock_id), Ok(Err(LockNotFoundByIdError(absent_id))) => { + assert_eq!(absent_id, lock_id); + }); +} + +/// Test cancelling a non-existent lock. +#[test] +fn test_cancel_nonexistent() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + + let transaction_context = plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: context.external.account_canonical_address(account_index_1), + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }; + let memo = CborMemo::Raw(vec![1u8, 2, 3].try_into().unwrap()); + let lock_id = LockId { + account_index: account_index_1.into(), + sequence_number: 999, + creation_order: 0, + }; + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_cancel( + lock_id.clone(), + Some(memo.clone()), + )])), + }, + }; + let summary = block_state + .execute_transaction(&mut context, transaction_context, account_index_1, payload) + .unwrap(); + assert_matches!(summary.outcome, TransactionOutcome::Rejected(TransactionRejectReason::NonExistentLockId(rejected_lock_id)) => { + assert_eq!(rejected_lock_id, lock_id); + }); +} + +/// Test that cancelling a lock is not blocked by token pause or deny-list restrictions. +#[test] +fn test_cancel_ignores_token_pause_and_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let canceller = context.external.create_account(); + + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default() + .mintable() + .burnable() + .deny_list(), + 2, + Some(RawTokenAmount::from(10000)), + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(500), + ); + + let lock_id = LockId { + account_index: owner.account_index().into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![owner.account_index()], + grants: vec![ + LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }, + LockControllerSimpleV0Grant { + account: canceller.account_index(), + roles: vec![LockControllerSimpleV0Capability::Cancel], + }, + ], + tokens: vec![token_id.clone()], + expiry: 1000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let fund_events = utils::execute_meta_operations( + &mut context, + &mut block_state, + owner.account_index(), + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + concordium_base::protocol_level_tokens::TokenAmount::from_raw(500, 2), + None, + )], + ); + assert_eq!(fund_events.len(), 1); + + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(owner_addr), + })], + ); + utils::pause_token( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + ); + + let events = utils::execute_meta_operations( + &mut context, + &mut block_state, + canceller.account_index(), + vec![lock_cancel(lock_id.clone(), None)], + ); + assert_eq!(events.len(), 2); + assert_matches!(&events[1], BlockItemEvent::LockDestroyed(LockDestroyEvent{lock_id: event_lock_id}) => { + assert_eq!(event_lock_id, &lock_id); + }); +} diff --git a/plt/plt-scheduler/tests/lock_create.rs b/plt/plt-scheduler/tests/lock_create.rs new file mode 100644 index 0000000000..937714c6da --- /dev/null +++ b/plt/plt-scheduler/tests/lock_create.rs @@ -0,0 +1,340 @@ +//! Tests for creating a PLT lock. + +use crate::utils::BlockStateLatest; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::{ + base::Energy, + common::{cbor, cbor::value::Value, types::TransactionTime}, + contracts_common::Duration, + protocol_level_locks::{ + LockConfig, LockController, LockControllerSimpleV0, LockControllerSimpleV0Capability, + LockControllerSimpleV0Grant, LockId, LockMetadata, LockRecipients, + }, + protocol_level_tokens::{ + MetadataUrl, RawCbor, TokenAmount, TokenId, TokenModuleInitializationParameters, + meta_operations::{MetaUpdatePayload, lock_create}, + }, + transactions::Payload, + updates::{CreatePlt, UpdatePayload}, +}; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::persistent::chain_parameters::p11::PersistentChainParametersP11; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::events::{BlockItemEvent, LockCreateEvent}; +use std::collections::HashMap; + +mod utils; + +fn execute_lock_create_with_duration( + expiry_seconds: u64, + block_timestamp: u64, + max_lock_duration: u64, +) -> ( + Result< + plt_scheduler_types::types::execution::TransactionExecutionSummary, + plt_scheduler::scheduler::TransactionExecutionError, + >, + bool, +) { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account_index = context.external.create_account().account_index(); + let account = context.external.account_canonical_address(account_index); + let lock_id = LockId::new(account_index, 1, 0); + let config = LockConfig { + recipients: LockRecipients::Any, + expiry: TransactionTime::from_seconds(expiry_seconds), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }), + metadata: None, + }; + let payload = MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![lock_create(config)])), + }; + let result = plt_scheduler::scheduler::p11::execute_transaction( + &mut context, + &mut block_state, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account, + transaction_sequence_number: 1.into(), + block_timestamp: block_timestamp.into(), + }, + Account::from_existing_account(account_index), + Payload::MetaUpdate { payload }, + &PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(max_lock_duration), + }, + ); + let lock_exists = matches!(block_state.lock_by_id(&context, &lock_id), Ok(Ok(_))); + (result, lock_exists) +} + +#[test] +fn test_create_simple_lock() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_1 = context.external.account_canonical_address(account_index_1); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Test PLT 1".to_owned()), + metadata: Some(MetadataUrl::from("https://pltX.token".to_string())), + governance_account: Some(account_1.into()), + allow_list: None, + deny_list: None, + initial_supply: Some(TokenAmount::from_raw(10000, 2)), + mintable: Some(true), + burnable: Some(true), + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: plt_x.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 2, + initialization_parameters, + }); + block_state + .execute_chain_update(&mut context, payload) + .expect("create pltX"); + + let metadata = LockMetadata { + name: Some("Test lock".to_string()), + description: Some("Lock created in scheduler test".to_string()), + additional: HashMap::from([("issuer".to_string(), Value::Text("Concordium".to_string()))]), + }; + let config = LockConfig { + recipients: LockRecipients::Limited(vec![account_1.into()]), + expiry: TransactionTime::from_seconds(1000), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: account_1.into(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![plt_x], + keep_alive: false, + memo: None, + }), + metadata: Some(metadata.encode_raw_cbor()), + }; + let operations = vec![lock_create(config.clone())]; + let payload = MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account_1, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + account_index_1, + Payload::MetaUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, plt_scheduler_types::types::execution::TransactionOutcome::Success(events) => events); + assert_eq!(events.len(), 1); + let lock_id = LockId::new(account_index_1, 1, 0); + assert_eq!( + events[0], + BlockItemEvent::LockCreated(LockCreateEvent { + lock_id: lock_id.clone(), + lock_config: RawCbor::from(cbor::cbor_encode(&config)) + }) + ); + + let stored_metadata = block_state + .lock_by_id(&context, &lock_id) + .unwrap() + .unwrap() + .lock_configuration(&context) + .unwrap() + .metadata + .clone(); + assert_eq!(stored_metadata, Some(metadata.encode_raw_cbor())); +} + +#[test] +fn test_create_any_recipient_lock() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let account_index_1 = context.external.create_account().account_index(); + let account_1 = context.external.account_canonical_address(account_index_1); + + let plt_x: TokenId = "pltX".parse().unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Test PLT 1".to_owned()), + metadata: Some(MetadataUrl::from("https://pltX.token".to_string())), + governance_account: Some(account_1.into()), + allow_list: None, + deny_list: None, + initial_supply: Some(TokenAmount::from_raw(10000, 2)), + mintable: Some(true), + burnable: Some(true), + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: plt_x.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 2, + initialization_parameters, + }); + block_state + .execute_chain_update(&mut context, payload) + .expect("create pltX"); + + let config = LockConfig { + recipients: LockRecipients::Any, + expiry: TransactionTime::from_seconds(1000), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants: vec![LockControllerSimpleV0Grant { + account: account_1.into(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![plt_x], + keep_alive: false, + memo: None, + }), + metadata: None, + }; + let operations = vec![lock_create(config.clone())]; + let payload = MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account_1, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + account_index_1, + Payload::MetaUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, plt_scheduler_types::types::execution::TransactionOutcome::Success(events) => events); + assert_eq!(events.len(), 1); + assert_eq!( + events[0], + BlockItemEvent::LockCreated(LockCreateEvent { + lock_id: LockId::new(account_index_1, 1, 0), + lock_config: RawCbor::from(cbor::cbor_encode(&config)) + }) + ); + + let lock = block_state + .lock_by_id(&context, &LockId::new(account_index_1, 1, 0)) + .expect("lock lookup must succeed") + .expect("lock must exist"); + let configuration = lock + .lock_configuration(&context) + .expect("lock configuration must load"); + assert!(configuration.recipients.is_any()); +} + +#[test] +fn lock_creation_enforces_expiry_and_maximum_duration() { + let lock_id = LockId::new(0, 1, 0); + + let (result, lock_exists) = execute_lock_create_with_duration(0, 1, u64::MAX); + let outcome = result.expect("expired lock creation must execute").outcome; + assert_matches!(outcome, plt_scheduler_types::types::execution::TransactionOutcome::Rejected( + plt_scheduler_types::types::reject_reasons::TransactionRejectReason::LockExpired(id) + ) if id == lock_id); + assert!(!lock_exists); + + let (result, lock_exists) = execute_lock_create_with_duration(1, 1_000, 0); + let outcome = result.expect("boundary lock creation must execute").outcome; + assert_matches!( + outcome, + plt_scheduler_types::types::execution::TransactionOutcome::Success(_) + ); + assert!(lock_exists); + + let (result, lock_exists) = execute_lock_create_with_duration(2, 1_000, 999); + let outcome = result.expect("overlong lock creation must execute").outcome; + assert_matches!(outcome, plt_scheduler_types::types::execution::TransactionOutcome::Rejected( + plt_scheduler_types::types::reject_reasons::TransactionRejectReason::LockDurationTooLong(id) + ) if id == lock_id); + assert!(!lock_exists); +} + +#[test] +fn lock_creation_maximum_deadline_inclusive() { + let (result, lock_exists) = execute_lock_create_with_duration(2, 1_500, 500); + let outcome = result.expect("deadline lock creation must execute").outcome; + assert_matches!( + outcome, + plt_scheduler_types::types::execution::TransactionOutcome::Success(_) + ); + assert!(lock_exists); + + let (result, lock_exists) = execute_lock_create_with_duration(2, 1_500, 499); + let outcome = result.expect("overlong lock creation must execute").outcome; + assert_matches!(outcome, plt_scheduler_types::types::execution::TransactionOutcome::Rejected( + plt_scheduler_types::types::reject_reasons::TransactionRejectReason::LockDurationTooLong(_) + )); + assert!(!lock_exists); +} + +#[test] +fn lock_creation_duration_reject_reports_its_creation_order() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account_index = context.external.create_account().account_index(); + let account = context.external.account_canonical_address(account_index); + let config = |expiry| LockConfig { + recipients: LockRecipients::Any, + expiry: TransactionTime::from_seconds(expiry), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants: vec![], + tokens: vec![], + keep_alive: false, + memo: None, + }), + metadata: None, + }; + let payload = MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&vec![ + lock_create(config(1)), + lock_create(config(2)), + ])), + }; + let result = plt_scheduler::scheduler::p11::execute_transaction( + &mut context, + &mut block_state, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + Account::from_existing_account(account_index), + Payload::MetaUpdate { payload }, + &PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(1_000), + }, + ) + .expect("multi-operation lock creation must execute"); + assert_matches!(result.outcome, plt_scheduler_types::types::execution::TransactionOutcome::Rejected( + plt_scheduler_types::types::reject_reasons::TransactionRejectReason::LockDurationTooLong(lock_id) + ) if lock_id == LockId::new(account_index, 1, 1)); +} diff --git a/plt/plt-scheduler/tests/lock_fund.rs b/plt/plt-scheduler/tests/lock_fund.rs new file mode 100644 index 0000000000..f38dd254e2 --- /dev/null +++ b/plt/plt-scheduler/tests/lock_fund.rs @@ -0,0 +1,351 @@ +//! Tests for funding protocol-level token locks. + +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_locks::LockInfo; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaUpdateOperations, MetaUpdatePayload, lock_fund, +}; +use concordium_base::protocol_level_tokens::{ + RawCbor, TokenAmount, TokenId, TokenModuleAccountState, TokenModuleRejectReason, +}; +use concordium_base::transactions::Payload; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::events::{BlockItemEvent, TokenTransferEvent}; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +mod utils; + +macro_rules! execute_meta_update { + ($context:expr, $block_state:expr, $sender:expr, $timestamp:expr, $operations:expr $(,)?) => {{ + let sender_addr = $context.external.account_canonical_address($sender); + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&MetaUpdateOperations { + operations: $operations, + })), + }, + }; + + $block_state + .execute_transaction( + $context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: $timestamp.into(), + }, + $sender, + payload, + ) + .expect("meta-update transaction must execute") + .outcome + }}; +} + +macro_rules! token_account_info { + ($context:expr, $block_state:expr, $account:expr, $token_id:expr $(,)?) => {{ + $block_state + .query_token_account_infos($context, $account) + .expect("token account query must succeed") + .into_iter() + .find(|info| &info.token_id == $token_id) + .expect("token account info must exist") + }}; +} + +macro_rules! token_module_account_state { + ($info:expr $(,)?) => {{ + cbor::cbor_decode::( + $info + .account_state + .module_state + .as_ref() + .expect("token account state must contain token-module state"), + ) + .expect("token-module account state must decode") + }}; +} + +#[test] +fn test_lock_fund_updates_account_and_lock_state() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + let events = assert_matches!(outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: event_token_id, + from, + to, + amount, + from_lock, + to_lock, + .. + }) => { + assert_eq!(event_token_id, &token_id); + assert_eq!(from, &TokenHolder::Account(sender_addr)); + assert_eq!(to, &TokenHolder::Account(sender_addr)); + assert_eq!(amount.amount, RawTokenAmount::from(250)); + assert_eq!(amount.decimals, 4); + assert_eq!(from_lock, &None); + assert_eq!(to_lock, &Some(lock_id.clone())); + }); + + let sender_info = + token_account_info!(&context, &block_state, sender.account_index(), &token_id); + assert_eq!( + sender_info.account_state.balance.amount, + RawTokenAmount::from(1000) + ); + let sender_state = token_module_account_state!(&sender_info); + assert_eq!(sender_state.available.unwrap().value(), 750); + assert_eq!(sender_state.locks.len(), 1); + assert_eq!(sender_state.locks[0].lock, lock_id); + assert_eq!(sender_state.locks[0].amount.value(), 250); + + let lock_info: LockInfo = cbor::cbor_decode( + block_state + .query_lock_info(&context, &lock_id) + .expect("lock info query must succeed"), + ) + .expect("lock info must decode"); + assert_eq!(lock_info.funds.len(), 1); + assert_eq!(lock_info.funds[0].amounts.len(), 1); + assert_eq!(lock_info.funds[0].amounts[0].token, token_id); + assert_eq!(lock_info.funds[0].amounts[0].amount.value(), 250); +} + +#[test] +fn test_lock_fund_rejects_when_amount_exceeds_available_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + sender.account_index(), + &token_id, + RawTokenAmount::from(250), + ); + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(800, 4), + None, + )], + ); + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::TokenBalanceInsufficient(reason) => { + assert_eq!(reason.available_balance, TokenAmount::from_raw(750, 4)); + assert_eq!(reason.required_balance, TokenAmount::from_raw(800, 4)); + }); +} + +#[test] +fn test_lock_fund_rejects_unauthorized_sender() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let other = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let other_addr = context + .external + .account_canonical_address(other.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + other.account_index(), + 0, + vec![lock_fund( + token_id, + lock_id.clone(), + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockFundNotAuthorized(lock_id, other_addr)); + }); +} + +#[test] +fn test_lock_fund_rejects_after_expiry() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 10, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 20_000, + vec![lock_fund( + token_id, + lock_id.clone(), + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockExpired(lock_id)); + }); +} diff --git a/plt/plt-scheduler/tests/lock_return.rs b/plt/plt-scheduler/tests/lock_return.rs new file mode 100644 index 0000000000..1b0dd16524 --- /dev/null +++ b/plt/plt-scheduler/tests/lock_return.rs @@ -0,0 +1,410 @@ +//! Tests for returning funds from protocol-level token locks. + +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_locks::LockInfo; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaUpdateOperations, MetaUpdatePayload, lock_fund, lock_return, +}; +use concordium_base::protocol_level_tokens::{ + RawCbor, TokenAmount, TokenId, TokenModuleAccountState, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::block_state::LockNotFoundByIdError; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::events::{BlockItemEvent, LockDestroyEvent, TokenTransferEvent}; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +mod utils; + +macro_rules! execute_meta_update { + ($context:expr, $block_state:expr, $sender:expr, $timestamp:expr, $operations:expr $(,)?) => {{ + let sender_addr = $context.external.account_canonical_address($sender); + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&MetaUpdateOperations { + operations: $operations, + })), + }, + }; + + $block_state + .execute_transaction( + $context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: $timestamp.into(), + }, + $sender, + payload, + ) + .expect("meta-update transaction must execute") + .outcome + }}; +} + +macro_rules! token_account_info { + ($context:expr, $block_state:expr, $account:expr, $token_id:expr $(,)?) => {{ + $block_state + .query_token_account_infos($context, $account) + .expect("token account query must succeed") + .into_iter() + .find(|info| &info.token_id == $token_id) + .expect("token account info must exist") + }}; +} + +macro_rules! token_module_account_state { + ($info:expr $(,)?) => {{ + cbor::cbor_decode::( + $info + .account_state + .module_state + .as_ref() + .expect("token account state must contain token-module state"), + ) + .expect("token-module account state must decode") + }}; +} + +#[test] +fn test_lock_return_deletes_empty_lock_when_keep_alive_is_false() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Return, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_return( + token_id.clone(), + lock_id.clone(), + sender_addr, + TokenAmount::from_raw(250, 4), + None, + )], + ); + let events = assert_matches!(outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 2); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: event_token_id, + from, + to, + amount, + from_lock, + to_lock, + .. + }) => { + assert_eq!(event_token_id, &token_id); + assert_eq!(from, &TokenHolder::Account(sender_addr)); + assert_eq!(to, &TokenHolder::Account(sender_addr)); + assert_eq!(amount.amount, RawTokenAmount::from(250)); + assert_eq!(amount.decimals, 4); + assert_eq!(from_lock, &Some(lock_id.clone())); + assert_eq!(to_lock, &None); + }); + assert_matches!(&events[1], BlockItemEvent::LockDestroyed(LockDestroyEvent { lock_id: event_lock_id }) => { + assert_eq!(event_lock_id, &lock_id); + }); + + let sender_info = + token_account_info!(&context, &block_state, sender.account_index(), &token_id); + assert_eq!( + sender_info.account_state.balance.amount, + RawTokenAmount::from(1000) + ); + let sender_state = token_module_account_state!(&sender_info); + assert!(sender_state.available.is_none()); + assert!(sender_state.locks.is_empty()); + + assert_matches!( + block_state.query_lock_info(&context, &lock_id), + Err(LockNotFoundByIdError(_)) + ); +} +#[test] +fn test_lock_return_keeps_empty_lock_when_keep_alive_is_true() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Return, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: true, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_return( + token_id.clone(), + lock_id.clone(), + sender_addr, + TokenAmount::from_raw(250, 4), + None, + )], + ); + let events = assert_matches!(outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(..)); + + let lock_info: LockInfo = cbor::cbor_decode( + block_state + .query_lock_info(&context, &lock_id) + .expect("lock info query must succeed"), + ) + .expect("lock info must decode"); + assert!(lock_info.funds.is_empty()); +} + +#[test] +fn test_lock_return_rejects_unauthorized_sender() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_return( + token_id, + lock_id.clone(), + owner_addr, + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockReturnNotAuthorized(lock_id, owner_addr)); + }); +} + +#[test] +fn test_lock_return_rejects_after_expiry() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Return, + ], + }], + tokens: vec![token_id.clone()], + expiry: 10, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 20_000, + vec![lock_return( + token_id, + lock_id.clone(), + owner_addr, + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockExpired(lock_id)); + }); +} diff --git a/plt/plt-scheduler/tests/lock_send.rs b/plt/plt-scheduler/tests/lock_send.rs new file mode 100644 index 0000000000..658caa2b6a --- /dev/null +++ b/plt/plt-scheduler/tests/lock_send.rs @@ -0,0 +1,1029 @@ +//! Tests for sending funds from protocol-level token locks. + +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_locks::LockInfo; +use concordium_base::protocol_level_locks::{ + LockConfig, LockController, LockControllerSimpleV0, LockControllerSimpleV0Capability, + LockControllerSimpleV0Grant as CborLockControllerSimpleV0Grant, LockId, LockRecipients, +}; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, OperationNotPermittedRejectReason, RawCbor, TokenAmount, TokenId, + TokenListUpdateDetails, TokenModuleAccountState, TokenModuleRejectReason, TokenOperation, + meta_operations::{MetaUpdateOperations, MetaUpdatePayload, lock_create, lock_fund, lock_send}, +}; +use concordium_base::transactions::Payload; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::events::{BlockItemEvent, TokenTransferEvent}; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +mod utils; + +macro_rules! execute_meta_update { + ($context:expr, $block_state:expr, $sender:expr, $timestamp:expr, $operations:expr $(,)?) => {{ + let sender_addr = $context.external.account_canonical_address($sender); + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&MetaUpdateOperations { + operations: $operations, + })), + }, + }; + + $block_state + .execute_transaction( + $context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: $timestamp.into(), + }, + $sender, + payload, + ) + .expect("meta-update transaction must execute") + .outcome + }}; +} + +macro_rules! token_account_info { + ($context:expr, $block_state:expr, $account:expr, $token_id:expr $(,)?) => {{ + $block_state + .query_token_account_infos($context, $account) + .expect("token account query must succeed") + .into_iter() + .find(|info| &info.token_id == $token_id) + .expect("token account info must exist") + }}; +} + +macro_rules! token_module_account_state { + ($info:expr $(,)?) => {{ + cbor::cbor_decode::( + $info + .account_state + .module_state + .as_ref() + .expect("token account state must contain token-module state"), + ) + .expect("token-module account state must decode") + }}; +} + +#[test] +fn test_lock_send_moves_locked_funds_to_recipient() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![ + lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + ), + lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + ) + ], + ); + let events = assert_matches!(outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 2); + assert_matches!(&events[1], BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: event_token_id, + from, + to, + amount, + from_lock, + to_lock, + .. + }) => { + assert_eq!(event_token_id, &token_id); + assert_eq!(from, &TokenHolder::Account(sender_addr)); + assert_eq!(to, &TokenHolder::Account(recipient_addr)); + assert_eq!(amount.amount, RawTokenAmount::from(100)); + assert_eq!(amount.decimals, 4); + assert_eq!(from_lock, &Some(lock_id.clone())); + assert_eq!(to_lock, &None); + }); + + let sender_info = + token_account_info!(&context, &block_state, sender.account_index(), &token_id); + assert_eq!( + sender_info.account_state.balance.amount, + RawTokenAmount::from(900) + ); + let sender_state = token_module_account_state!(&sender_info); + assert_eq!(sender_state.available.unwrap().value(), 750); + assert_eq!(sender_state.locks.len(), 1); + assert_eq!(sender_state.locks[0].amount.value(), 150); + + let recipient_info = + token_account_info!(&context, &block_state, recipient.account_index(), &token_id); + assert_eq!( + recipient_info.account_state.balance.amount, + RawTokenAmount::from(100) + ); + let recipient_state = token_module_account_state!(&recipient_info); + assert!(recipient_state.available.is_none()); + assert!(recipient_state.locks.is_empty()); + + let lock_info: LockInfo = cbor::cbor_decode( + block_state + .query_lock_info(&context, &lock_id) + .expect("lock info query must succeed"), + ) + .expect("lock info must decode"); + assert_eq!(lock_info.funds.len(), 1); + assert_eq!(lock_info.funds[0].amounts[0].amount.value(), 150); +} +#[test] +fn test_lock_send_allows_any_recipient() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let arbitrary_recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let recipient_addr = context + .external + .account_canonical_address(arbitrary_recipient.account_index()); + + let lock_id = LockId::new(owner.account_index(), 1u64, 0); // 1 matches the seq number given by `execute_meta_update` + let operations = vec![ + lock_create(LockConfig { + recipients: LockRecipients::Any, + expiry: 1_804_806_000.into(), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants: vec![CborLockControllerSimpleV0Grant { + account: CborHolderAccount::from(owner_addr), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + keep_alive: false, + memo: None, + }), + metadata: None, + }), + lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + ), + lock_send( + token_id.clone(), + lock_id.clone(), + owner_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + ), + ]; + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + operations, + ); + assert_matches!(outcome, TransactionOutcome::Success(_)); + + let lock_info: LockInfo = cbor::cbor_decode( + block_state + .query_lock_info(&context, &lock_id) + .expect("lock info query must succeed"), + ) + .expect("lock info must decode"); + assert_eq!(lock_info.recipients, LockRecipients::Any); +} + +#[test] +fn test_lock_send_rejects_non_recipient() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let non_recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let non_recipient_addr = context + .external + .account_canonical_address(non_recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + owner_addr, + non_recipient_addr, + TokenAmount::from_raw(1, 4), + None, + )], + ); + + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockRecipientNotPermitted(lock_id, non_recipient_addr)); + }); +} +#[test] +fn test_lock_send_sender_not_in_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().allow_list(), + 4, + None, + ); + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![ + TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }), + TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(sender_addr), + }), + TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(recipient_addr), + }), + ], + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + assert_matches!( + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ), + TransactionOutcome::Success(_) + ); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::RemoveAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(sender_addr), + })], + ); + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + )], + ); + + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender not in allow list"); + } + ); +} +#[test] +fn test_lock_send_recipient_not_in_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().allow_list(), + 4, + None, + ); + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![ + TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }), + TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(sender_addr), + }), + ], + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + assert_matches!( + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ), + TransactionOutcome::Success(_) + ); + + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + )], + ); + + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(recipient_addr)); + assert_eq!(reason, "recipient not in allow list"); + } + ); +} +#[test] +fn test_lock_send_sender_in_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().deny_list(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + assert_matches!( + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ), + TransactionOutcome::Success(_) + ); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(sender_addr), + })], + ); + + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + )], + ); + + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender in deny list"); + } + ); +} +#[test] +fn test_lock_send_recipient_in_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().deny_list(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + assert_matches!( + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ), + TransactionOutcome::Success(_) + ); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(recipient_addr), + })], + ); + + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + )], + ); + + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(recipient_addr)); + assert_eq!(reason, "recipient in deny list"); + } + ); +} +#[test] +fn test_lock_send_rejects_when_token_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let sender = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + assert_matches!( + execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ), + TransactionOutcome::Success(_) + ); + utils::pause_token( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + ); + + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + sender.account_index(), + 0, + vec![lock_send( + token_id.clone(), + lock_id.clone(), + sender_addr, + recipient_addr, + TokenAmount::from_raw(100, 4), + None, + )], + ); + + let reject_reason = assert_matches!(outcome, TransactionOutcome::Rejected(reason) => reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: None, + reason: Some(reason), + }) if reason == "token operation transfer is paused" + ); +} + +#[test] +fn test_lock_send_rejects_unauthorized_sender() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_send( + token_id, + lock_id.clone(), + owner_addr, + recipient_addr, + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockSendNotAuthorized(lock_id, owner_addr)); + }); +} + +#[test] +fn test_lock_send_rejects_after_expiry() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "pltX".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + owner.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(owner.account_index(), 7u64, 0); + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: owner.account_index(), + roles: vec![ + LockControllerSimpleV0Capability::Fund, + LockControllerSimpleV0Capability::Send, + ], + }], + tokens: vec![token_id.clone()], + expiry: 10, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 0, + vec![lock_fund( + token_id.clone(), + lock_id.clone(), + TokenAmount::from_raw(250, 4), + None, + )], + ); + + let owner_addr = context + .external + .account_canonical_address(owner.account_index()); + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let outcome = execute_meta_update!( + &mut context, + &mut block_state, + owner.account_index(), + 20_000, + vec![lock_send( + token_id, + lock_id.clone(), + owner_addr, + recipient_addr, + TokenAmount::from_raw(1, 4), + None, + )], + ); + assert_matches!(outcome, TransactionOutcome::Rejected(reason) => { + assert_eq!(reason, TransactionRejectReason::LockExpired(lock_id)); + }); +} diff --git a/plt/plt-scheduler/tests/meta_transaction.rs b/plt/plt-scheduler/tests/meta_transaction.rs new file mode 100644 index 0000000000..3ffc53a093 --- /dev/null +++ b/plt/plt-scheduler/tests/meta_transaction.rs @@ -0,0 +1,294 @@ +//! Tests for the meta-update transaction execution logic. + +use std::str::FromStr; + +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborMemo, MetadataUrl, RawCbor, TokenAmount, TokenId, TokenListUpdateEventDetails, + TokenModuleInitializationParameters, TokenPauseDetails, TokenPauseEventDetails, + meta_operations, +}; +use concordium_base::transactions::Payload; +use concordium_base::updates::{CreatePlt, UpdatePayload}; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::entity::entity_test_stub::StubbedEntityContext; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::events::{ + self, BlockItemEvent, EncodedTokenModuleEvent, TokenBurnEvent, TokenTransferEvent, +}; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::{self, TokenHolder}; + +use crate::utils::BlockStateLatest; +use crate::utils::entity_traits::scheduler::SchedulerOperations; + +mod utils; + +const PLT_X: &str = "pltX"; +const PLT_Y: &str = "pltY"; + +fn setup_test_plts( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateLatest, + account_1: &Account, +) { + let account_1_addr = context + .external + .account_canonical_address(account_1.account_index()); + + // Set up PLT `pltX`. + let plt_x: TokenId = PLT_X.parse().unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Test PLT 1".to_owned()), + metadata: Some(MetadataUrl::from("https://pltX.token".to_string())), + governance_account: Some(account_1_addr.into()), + allow_list: None, + deny_list: None, + initial_supply: Some(TokenAmount::from_raw(10000, 2)), + mintable: Some(true), + burnable: Some(true), + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: plt_x.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 2, + initialization_parameters, + }); + block_state + .execute_chain_update(context, payload) + .expect("create pltX"); + + // Set up PLT `pltY`. + let plt_y: TokenId = PLT_Y.parse().unwrap(); + let parameters = TokenModuleInitializationParameters { + name: Some("Test PLT 2".to_owned()), + metadata: Some(MetadataUrl::from("https://pltY.token".to_string())), + governance_account: Some(account_1_addr.into()), + allow_list: Some(true), + deny_list: Some(true), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: plt_y.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters, + }); + block_state + .execute_chain_update(context, payload) + .expect("create pltY"); +} + +#[test] +fn test_meta_update_transaction() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + // Set up initial accounts. + let account_1 = context.external.create_account(); + let account_2 = context.external.create_account(); + let account_1_addr = context + .external + .account_canonical_address(account_1.account_index()); + let account_2_addr = context + .external + .account_canonical_address(account_2.account_index()); + setup_test_plts(&mut context, &mut block_state, &account_1); + let plt_x: TokenId = PLT_X.parse().unwrap(); + let plt_y: TokenId = PLT_Y.parse().unwrap(); + + use meta_operations::*; + let operations = vec![ + transfer_tokens(plt_x.clone(), account_2_addr, TokenAmount::from_raw(100, 2)), + mint_tokens(plt_y.clone(), TokenAmount::from_raw(100000, 0)), + pause(plt_x.clone()), + add_token_allow_list(plt_y.clone(), account_2_addr), + add_token_deny_list(plt_y.clone(), account_1_addr), + add_token_allow_list(plt_y.clone(), account_1_addr), + remove_token_deny_list(plt_y.clone(), account_1_addr), + transfer_tokens_with_memo( + plt_y.clone(), + account_2_addr, + TokenAmount::from_raw(2200, 0), + CborMemo::Cbor(vec![0xa0u8].try_into().unwrap()), + ), + unpause(plt_x.clone()), + burn_tokens(plt_x.clone(), TokenAmount::from_raw(10, 2)), + remove_token_allow_list(plt_y.clone(), account_1_addr), + ]; + + let payload = meta_operations::MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account_1_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + account_1.account_index(), + Payload::MetaUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + assert_eq!(events.len(), 11); + assert_eq!( + events[0], + BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: plt_x.clone(), + from: TokenHolder::Account(account_1_addr), + to: TokenHolder::Account(account_2_addr), + amount: tokens::TokenAmount::from_raw(100, 2), + memo: None, + from_lock: None, + to_lock: None, + }) + ); + assert_eq!( + events[1], + BlockItemEvent::TokenMint(events::TokenMintEvent { + token_id: plt_y.clone(), + target: TokenHolder::Account(account_1_addr), + amount: tokens::TokenAmount::from_raw(100000, 0), + }) + ); + assert_eq!( + events[2], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_x.clone(), + event_type: "pause".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenPauseDetails {}).into(), + }) + ); + assert_eq!( + events[3], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_y.clone(), + event_type: "addAllowList".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenListUpdateEventDetails { + target: account_2_addr.into(), + }) + .into(), + }) + ); + assert_eq!( + events[4], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_y.clone(), + event_type: "addDenyList".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenListUpdateEventDetails { + target: account_1_addr.into(), + }) + .into(), + }) + ); + assert_eq!( + events[5], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_y.clone(), + event_type: "addAllowList".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenListUpdateEventDetails { + target: account_1_addr.into(), + }) + .into(), + }) + ); + assert_eq!( + events[6], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_y.clone(), + event_type: "removeDenyList".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenListUpdateEventDetails { + target: account_1_addr.into(), + }) + .into(), + }) + ); + assert_eq!( + events[7], + BlockItemEvent::TokenTransfer(TokenTransferEvent { + token_id: plt_y.clone(), + from: TokenHolder::Account(account_1_addr), + to: TokenHolder::Account(account_2_addr), + amount: tokens::TokenAmount::from_raw(2200, 0), + memo: Some(vec![0xa0u8].try_into().unwrap()), + from_lock: None, + to_lock: None, + }) + ); + assert_eq!( + events[8], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_x.clone(), + event_type: "unpause".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenPauseEventDetails {}).into(), + }) + ); + assert_eq!( + events[9], + BlockItemEvent::TokenBurn(TokenBurnEvent { + token_id: plt_x.clone(), + target: TokenHolder::Account(account_1_addr), + amount: tokens::TokenAmount::from_raw(10, 2), + }) + ); + assert_eq!( + events[10], + BlockItemEvent::TokenModule(EncodedTokenModuleEvent { + token_id: plt_y.clone(), + event_type: "removeAllowList".to_string().try_into().unwrap(), + details: cbor::cbor_encode(&TokenListUpdateEventDetails { + target: account_1_addr.into(), + }) + .into(), + }) + ); +} + +#[test] +fn test_meta_update_transaction_cbor_extra_fields() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + // Set up initial account. + let account_1 = context.external.create_account(); + let account_1_addr = context + .external + .account_canonical_address(account_1.account_index()); + use meta_operations::*; + + let payload = MetaUpdatePayload { + operations: RawCbor::from_str("81a1687472616e73666572a5646d656d6f440102030465746f6b656e68746f6b656e69643166616d6f756e74c482211a000186a069726563697069656e74d99d73a201d99d71a1011903970358200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2064626c616801").unwrap(), + }; + payload + .decode_operations() + .expect("should decode successfully even with extra fields in the CBOR"); + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: account_1_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + account_1.account_index(), + Payload::MetaUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!( + result.outcome, + TransactionOutcome::Rejected(TransactionRejectReason::SerializationFailure) + ); +} diff --git a/plt/plt-scheduler/tests/migration_p10_to_p11.rs b/plt/plt-scheduler/tests/migration_p10_to_p11.rs new file mode 100644 index 0000000000..b9322f2c7d --- /dev/null +++ b/plt/plt-scheduler/tests/migration_p10_to_p11.rs @@ -0,0 +1,106 @@ +//! Scheduler-level smoke test for block state migration from P10 to P11. +//! +//! This complements the block state migration unit test in the `plt-block-state` crate. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, TokenAmount, TokenAuthorizations, TokenId, TokenOperation, + TokenSupplyUpdateDetails, +}; +use plt_block_state::entity::block_state::migration; +use plt_block_state::entity::block_state::p10::BlockStateP10; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenAmount as QueryTokenAmount}; + +mod utils; + +const DECIMALS: u8 = 2; + +/// Smoke test of migrating a P10 block state to P11 at the scheduler level. +/// +/// P11 introduces a new authorization (roles) model which we specifically test. +#[test] +fn test_migrate_p10_to_p11() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + + // Create the token on the block state being migrated from (P10). + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _token_index) = utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + DECIMALS, + None, + ); + + // Migrate the block state from P10 to P11. + let (mut context, mut block_state) = + migration::test_utils::migrate_p10_to_p11(&mut context, block_state); + + // Query the migrated state (P11) using the scheduler-implemented queries. + assert_eq!(block_state.query_plt_list(&context), vec![token_id.clone()]); + let token_info = block_state + .query_token_info(&context, &token_id) + .expect("token info is queryable after migration"); + assert_eq!(token_info.token_id, token_id); + assert_eq!(token_info.state.token_module_ref, TOKEN_MODULE_REF); + assert_eq!(token_info.state.decimals, DECIMALS); + assert_eq!( + token_info.state.total_supply, + QueryTokenAmount { + amount: RawTokenAmount::from(0), + decimals: DECIMALS, + } + ); + + // Assert that the governance account was migrated into the new authorization model with the + // expected roles. + let authorizations = block_state + .query_token_authorizations(&context, &token_id) + .expect("token authorizations are queryable after migration"); + assert_eq!(authorizations.token_id, token_id); + let details: TokenAuthorizations = cbor::cbor_decode(&authorizations.details).unwrap(); + let gov_holder = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + assert_eq!( + details.update_admin_roles.unwrap().accounts, + vec![gov_holder.clone()] + ); + assert_eq!( + details.update_metadata.unwrap().accounts, + vec![gov_holder.clone()] + ); + assert_eq!(details.pause.unwrap().accounts, vec![gov_holder.clone()]); + assert_eq!(details.mint.unwrap().accounts, vec![gov_holder.clone()]); + assert_eq!(details.burn.unwrap().accounts, vec![gov_holder]); + // No allow/deny list was configured, so those authorizations are absent. + assert!(details.update_allow_list.is_none()); + assert!(details.update_deny_list.is_none()); + + // The governance account must retain the permissions to run the expected transactions under the + // new authorization model: mint (requires the `Mint` role) and pause (requires the `Pause` + // role). + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(100, DECIMALS), + })], + ); + utils::pause_token( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + ); +} diff --git a/plt/plt-scheduler/tests/migration_p9_to_p10.rs b/plt/plt-scheduler/tests/migration_p9_to_p10.rs new file mode 100644 index 0000000000..7be16620ce --- /dev/null +++ b/plt/plt-scheduler/tests/migration_p9_to_p10.rs @@ -0,0 +1,67 @@ +//! Scheduler-level smoke test for block state migration from P9 to P10. +//! +//! This complements the block state migration unit test in the `plt-block-state` crate. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use concordium_base::protocol_level_tokens::{ + TokenAmount, TokenId, TokenOperation, TokenSupplyUpdateDetails, +}; +use plt_block_state::entity::block_state::migration; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenAmount as QueryTokenAmount}; + +mod utils; + +const DECIMALS: u8 = 2; + +/// Smoke test of migrating a P9 block state to P10 at the scheduler level. +#[test] +fn test_migrate_p9_to_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP9::default(); + + // Create the token on the block state being migrated from (P9). + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _token_index) = utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + DECIMALS, + None, + ); + + // Migrate the block state from P9 to P10. + let (mut context, mut block_state) = + migration::test_utils::migrate_p9_to_p10(&mut context, block_state); + + // Query the migrated state (P10) using the scheduler-implemented queries. + assert_eq!(block_state.query_plt_list(&context), vec![token_id.clone()]); + let token_info = block_state + .query_token_info(&context, &token_id) + .expect("token info is queryable after migration"); + assert_eq!(token_info.token_id, token_id); + assert_eq!(token_info.state.token_module_ref, TOKEN_MODULE_REF); + assert_eq!(token_info.state.decimals, DECIMALS); + assert_eq!( + token_info.state.total_supply, + QueryTokenAmount { + amount: RawTokenAmount::from(0), + decimals: DECIMALS, + } + ); + + // Execute a transaction on the migrated state. + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(100, DECIMALS), + })], + ); +} diff --git a/plt/plt-scheduler/tests/plt_block_state_stub.rs b/plt/plt-scheduler/tests/plt_block_state_stub.rs new file mode 100644 index 0000000000..cda61b0238 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_block_state_stub.rs @@ -0,0 +1,105 @@ +//! Tests for the block state stub infrastructure used in the plt-scheduler integration tests. + +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use concordium_base::base::AccountIndex; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::TokenId; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +mod utils; + +/// Test lookup account address and account from address. +#[test] +fn test_account_lookup_address() { + let mut context = entity_test_stub::new_stubbed_context(); + + let account = context.external.create_account(); + let address = context + .external + .account_canonical_address(account.account_index()); + + context + .account_by_address(&address) + .expect("Account is expected to exist"); + assert!( + context + .account_by_address(&AccountAddress([2u8; 32])) + .is_err(), + "Account is not expected to exist" + ); +} + +/// Test lookup account index and account from index. +#[test] +fn test_account_lookup_index() { + let mut context = entity_test_stub::new_stubbed_context(); + + let account = context.external.create_account(); + + context + .account_by_index(account.account_index()) + .expect("Account is expected to exist"); + assert!( + context.account_by_index(AccountIndex::from(2u64)).is_err(), + "Account is not expected to exist" + ); +} + +/// Test get account token balance. +#[test] +fn test_account_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + let account0 = context.external.create_account(); + let account1 = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account0.account_index(), + &token_id, + RawTokenAmount::from(245), + ); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + assert_eq!( + account0.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(245) + ); + assert_eq!( + account1.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(0) + ); +} + +/// Test looking up account by alias. +#[test] +fn test_account_by_alias() { + let mut context = entity_test_stub::new_stubbed_context(); + + let account = context.external.create_account(); + let account_address = context + .external + .account_canonical_address(account.account_index()); + let account_by_alias = context + .account_by_address(&account_address.get_alias(0).unwrap()) + .unwrap(); + + assert_eq!(account.account_index(), account_by_alias.account_index()); +} diff --git a/plt/plt-scheduler/tests/plt_create.rs b/plt/plt-scheduler/tests/plt_create.rs new file mode 100644 index 0000000000..8f9c255a0e --- /dev/null +++ b/plt/plt-scheduler/tests/plt_create.rs @@ -0,0 +1,339 @@ +//! Test of creating protocol-level token. Detailed tests should generally be implemented in +//! the tests of the token module in the `plt-token-module` crate. In the present file, +//! higher level tests are implemented. + +use crate::utils::BlockStateLatest; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, MetadataUrl, RawCbor, TokenAmount, TokenId, + TokenModuleInitializationParameters, TokenModuleRef, +}; +use concordium_base::updates::{CreatePlt, UpdatePayload}; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, FailureKind}; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +mod utils; + +/// Test create protocol-level token. +#[test] +fn test_plt_create() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + assert_eq!(context.external.plt_update_instruction_sequence_number(), 0); + + let token_id: TokenId = "testtokenid".parse().unwrap(); + + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 4, + initialization_parameters, + }); + let outcome = block_state + .execute_chain_update(&mut context, payload) + .expect("create and initialize token"); + let events = assert_matches!(outcome, ChainUpdateOutcome::Success(events) => events); + + // Assert update instruction sequence number incremented + assert_eq!(context.external.plt_update_instruction_sequence_number(), 1); + + // Assert token module state + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + assert_eq!( + token + .token_p9_base + .token_configuration(&context) + .unwrap() + .token_id, + token_id + ); + assert_eq!( + token + .token_p9_base + .token_configuration(&context) + .unwrap() + .decimals, + 4 + ); + assert_eq!( + token + .token_p9_base + .token_configuration(&context) + .unwrap() + .module_ref, + TOKEN_MODULE_REF + ); + + // Assert circulating supply and governance account balance + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + assert_eq!( + gov_account.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(0) + ); + + // Assert create token event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenCreated(create) => { + assert_eq!(create.payload.token_id, token_id); + }); +} + +/// Test create protocol-level token. +#[test] +fn test_plt_create_with_minting() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + assert_eq!(context.external.plt_update_instruction_sequence_number(), 0); + + let token_id: TokenId = "testtokenid".parse().unwrap(); + + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: Some(TokenAmount::from_raw(5000, 4)), + mintable: None, + burnable: None, + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 4, + initialization_parameters, + }); + let outcome = block_state + .execute_chain_update(&mut context, payload) + .expect("create and initialize token"); + let events = assert_matches!(outcome, ChainUpdateOutcome::Success(events) => events); + + // Assert update instruction sequence number incremented + assert_eq!(context.external.plt_update_instruction_sequence_number(), 1); + + // Assert circulating supply and governance account balance + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(5000) + ); + + // Assert create token and mint event + assert_eq!(events.len(), 2); + assert_matches!(&events[0], BlockItemEvent::TokenCreated(create) => { + assert_eq!(create.payload.token_id, token_id); + }); + assert_matches!(&events[1], BlockItemEvent::TokenMint(mint) => { + assert_eq!(mint.token_id, token_id); + assert_eq!(mint.amount.amount, RawTokenAmount::from(5000)); + assert_eq!(mint.amount.decimals, 4); + assert_eq!(mint.target, TokenHolder::Account(context.external.account_canonical_address(gov_account.account_index()))); + }); +} + +/// Test create protocol-level token where the token id is already used. Two token +/// ids which only differ in casing are considered equal. +#[test] +fn test_plt_create_duplicate_id() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + }; + let initialization_parameters: RawCbor = cbor::cbor_encode(¶meters).into(); + + let token_id1: TokenId = "TestTokenId".parse().unwrap(); + let payload1 = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id1.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 4, + initialization_parameters: initialization_parameters.clone(), + }); + + // Create first token + block_state + .execute_chain_update(&mut context, payload1) + .expect("create and initialize token"); + + // Try to use same token id just with different casing + let token_id2: TokenId = "testtokenid".parse().unwrap(); + let payload2 = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id2.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 4, + initialization_parameters, + }); + + // Create second token + let outcome = block_state + .execute_chain_update(&mut context, payload2) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + + assert_matches!( + failure_kind, + FailureKind::DuplicateTokenId(token_id) => { + assert_eq!(token_id, token_id1); + } + ); +} + +/// Test create protocol-level token where the token module reference is to an unknown token module. +#[test] +fn test_plt_create_unknown_token_module_reference() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + }; + let initialization_parameters: RawCbor = cbor::cbor_encode(¶meters).into(); + + let token_id: TokenId = "testtokenid".parse().unwrap(); + let unknown_module_ref = TokenModuleRef::new([0u8; 32]); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: unknown_module_ref, + decimals: 4, + initialization_parameters: initialization_parameters.clone(), + }); + + let outcome = block_state + .execute_chain_update(&mut context, payload) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + + assert_matches!( + failure_kind, + FailureKind::InvalidTokenModuleRef(module_ref) => { + assert_eq!(module_ref, unknown_module_ref); + } + ); +} + +/// Test create protocol-level token where the token module returns an error. +#[test] +fn test_plt_create_token_module_initialization_error() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + // No name specified + name: None, + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + }; + let initialization_parameters: RawCbor = cbor::cbor_encode(¶meters).into(); + + let token_id: TokenId = "testtokenid".parse().unwrap(); + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 4, + initialization_parameters: initialization_parameters.clone(), + }); + + let outcome = block_state + .execute_chain_update(&mut context, payload) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("Token name is missing"), "err: {}", err); + } + ); +} diff --git a/plt/plt-scheduler/tests/plt_lock_queries.rs b/plt/plt-scheduler/tests/plt_lock_queries.rs new file mode 100644 index 0000000000..616d9b65a1 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_lock_queries.rs @@ -0,0 +1,208 @@ +//! Tests for the new `query_lock_list` and `query_lock_info` scheduler query functions. + +use crate::utils::BlockStateLatest; +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::common::cbor::value::Value; +use concordium_base::common::types::TransactionTime; +use concordium_base::protocol_level_locks::LockInfo; +use concordium_base::protocol_level_locks::{ + LockController, LockControllerSimpleV0Capability, LockId, LockMetadata, LockRecipients, +}; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaUpdateOperations, MetaUpdatePayload, lock_create, +}; +use concordium_base::protocol_level_tokens::{CborHolderAccount, RawCbor, TokenId}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::block_state::LockNotFoundByIdError; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant; +use plt_scheduler_types::types::tokens::RawTokenAmount; +use std::collections::HashMap; + +mod utils; + +/// `query_lock_info` produces the canonical CBOR encoding of the assembled +/// `LockInfo` value, and the round-trip via `cbor_decode` recovers an equal value. +#[test] +fn test_query_lock_info_cbor_round_trip_with_funded_balances() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let recipient = context.external.create_account(); + let funding_account = context.external.create_account(); + let recipient_addr = context + .external + .account_canonical_address(recipient.account_index()); + let funding_addr = context + .external + .account_canonical_address(funding_account.account_index()); + + let token_id: TokenId = "TokenLockA".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + Some(RawTokenAmount::from(0)), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + funding_account.account_index(), + &token_id, + RawTokenAmount::from(123_400), + ); + + let lock_id = LockId { + account_index: funding_account.account_index().into(), + sequence_number: 1, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: funding_account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + funding_account.account_index(), + &token_id, + RawTokenAmount::from(100), + ); + + let bytes = block_state + .query_lock_info(&context, &lock_id) + .expect("query_lock_info must succeed for an existing lock"); + let decoded: LockInfo = + cbor::cbor_decode(&bytes).expect("CBOR encoding produced by query_lock_info must decode"); + + assert_eq!(decoded.lock, lock_id); + assert_eq!( + decoded.recipients, + LockRecipients::Limited(vec![CborHolderAccount::from(recipient_addr)]) + ); + assert_eq!(decoded.expiry, TransactionTime::from(1_804_806_000)); + assert_matches!(decoded.controller, LockController::SimpleV0(simple) => { + assert_eq!(simple.grants.len(), 1); + assert_eq!(simple.grants[0].account, CborHolderAccount::from(funding_addr)); + assert_eq!(simple.grants[0].roles, vec![LockControllerSimpleV0Capability::Fund]); + assert_eq!(simple.tokens, vec![token_id.clone()]); + assert!(!simple.keep_alive); + assert!(simple.memo.is_none()); + }); + assert_eq!(decoded.funds.len(), 1); + assert_eq!( + decoded.funds[0].account, + CborHolderAccount::from(funding_addr) + ); + assert_eq!(decoded.funds[0].amounts.len(), 1); + assert_eq!(decoded.funds[0].amounts[0].token, token_id); + assert_eq!(decoded.funds[0].amounts[0].amount.value(), 100); + assert_eq!(decoded.funds[0].amounts[0].amount.decimals(), 2); +} + +/// `query_lock_info` returns [`QueryLockError::LockDoesNotExist`] when the +/// lock id is not present in the block state. +#[test] +fn test_query_lock_info_any_recipient() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let owner = context.external.create_account(); + let token_id: TokenId = "TokenLockAny".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + Some(RawTokenAmount::from(0)), + ); + + let lock_id = LockId { + account_index: owner.account_index().into(), + sequence_number: 1, + creation_order: 0, + }; + let sender_addr = context + .external + .account_canonical_address(owner.account_index()); + let metadata = LockMetadata { + name: Some("Any recipient lock".to_string()), + description: Some("Metadata returned by GetLockInfo".to_string()), + additional: HashMap::from([("purpose".to_string(), Value::Text("query test".to_string()))]), + }; + let operations = MetaUpdateOperations { + operations: vec![lock_create( + concordium_base::protocol_level_locks::LockConfig { + recipients: LockRecipients::Any, + expiry: TransactionTime::from(1_804_806_000u64), + controller: LockController::SimpleV0( + concordium_base::protocol_level_locks::LockControllerSimpleV0 { + grants: vec![ + concordium_base::protocol_level_locks::LockControllerSimpleV0Grant { + account: CborHolderAccount::from(sender_addr), + roles: vec![LockControllerSimpleV0Capability::Fund], + }, + ], + tokens: vec![token_id.clone()], + keep_alive: false, + memo: None, + }, + ), + metadata: Some(metadata.encode_raw_cbor()), + }, + )], + }; + block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: concordium_base::base::Energy::from(u64::MAX), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + owner.account_index(), + Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }, + }, + ) + .expect("create any-recipient lock must succeed"); + + let bytes = block_state + .query_lock_info(&context, &lock_id) + .expect("query_lock_info must succeed for an existing lock"); + let decoded: LockInfo = + cbor::cbor_decode(&bytes).expect("CBOR encoding produced by query_lock_info must decode"); + + assert_eq!(decoded.recipients, LockRecipients::Any); + assert_eq!(decoded.metadata, Some(metadata.encode_raw_cbor())); +} + +#[test] +fn test_query_lock_info_unknown_lock() { + let context = entity_test_stub::new_stubbed_context(); + let block_state = BlockStateLatest::default(); + let unknown = LockId { + account_index: 999, + sequence_number: 999, + creation_order: 0, + }; + let result = block_state.query_lock_info(&context, &unknown); + assert_matches!(result, Err(LockNotFoundByIdError(_))); +} diff --git a/plt/plt-scheduler/tests/plt_queries.rs b/plt/plt-scheduler/tests/plt_queries.rs new file mode 100644 index 0000000000..bd04020119 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_queries.rs @@ -0,0 +1,461 @@ +//! Test of protocol-level token queries. Notice that detailed test of the token module queries are +//! implemented in the `plt-token-module` crate. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, RawCbor, TokenId, TokenListUpdateDetails, TokenModuleAccountState, + TokenModuleState, TokenOperation, TokenOperationsPayload, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::entity_test_stub; +use plt_block_state::persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test query token state +#[test] +fn test_query_plt_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id1: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id1.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + let token_id2: TokenId = "TokenId2".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id2.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + + let plts = block_state.query_plt_list(&context); + assert_eq!(plts, vec![token_id1, token_id2]); +} + +/// Test query token info +#[test] +fn test_query_token_info() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + + let non_canonical_token_id = "toKeniD1".parse().unwrap(); + // Lookup by token id that is not in canonical casing + let token_info = block_state + .query_token_info(&context, &non_canonical_token_id) + .unwrap(); + // Assert that the token id returned is in the canonical casing + assert_eq!(token_info.token_id, token_id); + assert_eq!(token_info.state.decimals, 4); + assert_eq!( + token_info.state.total_supply.amount, + RawTokenAmount::from(0) + ); + assert_eq!(token_info.state.total_supply.decimals, 4); + assert_eq!(token_info.state.token_module_ref, TOKEN_MODULE_REF); + let token_module_state: TokenModuleState = + cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!( + token_module_state.name.as_deref(), + Some("Protocol-level token") + ); +} + +/// Test query token account info +#[test] +fn test_query_token_account_info() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account = context.external.create_account(); + let token_id1: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id1.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let token_id2: TokenId = "TokenId2".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id2.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let token_id3: TokenId = "TokenId3".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id3, + TokenInitTestParams::default(), + 4, + None, + ); + + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account.account_index(), + &token_id1, + RawTokenAmount::from(1000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account.account_index(), + &token_id2, + RawTokenAmount::from(2000), + ); + + // Lookup account token infos + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + assert_eq!(token_account_infos.len(), 2); + assert_eq!(token_account_infos[0].token_id, token_id1); + assert_eq!( + token_account_infos[0].account_state.balance.amount, + RawTokenAmount::from(1000) + ); + assert_eq!(token_account_infos[0].account_state.balance.decimals, 4); + assert_eq!(token_account_infos[1].token_id, token_id2); + assert_eq!( + token_account_infos[1].account_state.balance.amount, + RawTokenAmount::from(2000) + ); + assert_eq!(token_account_infos[1].account_state.balance.decimals, 4); +} + +/// Test query token account info reports available balance excluding locked funds. +#[test] +fn test_query_token_account_info_available_with_locked_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId { + account_index: account.account_index().into(), + sequence_number: 1, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + account.account_index(), + &token_id, + RawTokenAmount::from(250), + ); + + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + assert_eq!(token_account_infos.len(), 1); + assert_eq!(token_account_infos[0].token_id, token_id); + let module_state: TokenModuleAccountState = cbor::cbor_decode( + token_account_infos[0] + .account_state + .module_state + .as_ref() + .unwrap(), + ) + .unwrap(); + let available = module_state.available.unwrap(); + assert_eq!(available.value(), 750); + assert_eq!(available.decimals(), 4); + assert_eq!(module_state.locks.len(), 1); + assert_eq!(module_state.locks[0].lock, lock_id); + assert_eq!(module_state.locks[0].amount.value(), 250); + assert_eq!(module_state.locks[0].amount.decimals(), 4); +} + +/// Test query token account info sums multiple locks for the same account/token pair. +#[test] +fn test_query_token_account_info_available_with_multiple_locks() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id1 = LockId { + account_index: account.account_index().into(), + sequence_number: 1, + creation_order: 0, + }; + let lock_config1 = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id1, lock_config1); + let lock_id2 = LockId { + account_index: account.account_index().into(), + sequence_number: 2, + creation_order: 0, + }; + let lock_config2 = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id2, lock_config2); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id1, + account.account_index(), + &token_id, + RawTokenAmount::from(250), + ); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id2, + account.account_index(), + &token_id, + RawTokenAmount::from(300), + ); + + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + let module_state: TokenModuleAccountState = cbor::cbor_decode( + token_account_infos[0] + .account_state + .module_state + .as_ref() + .unwrap(), + ) + .unwrap(); + let available = module_state.available.unwrap(); + assert_eq!(available.value(), 450); + assert_eq!(available.decimals(), 4); + assert_eq!(module_state.locks.len(), 2); + assert_eq!(module_state.locks[0].lock, lock_id1); + assert_eq!(module_state.locks[0].amount.value(), 250); + assert_eq!(module_state.locks[0].amount.decimals(), 4); + assert_eq!(module_state.locks[1].lock, lock_id2); + assert_eq!(module_state.locks[1].amount.value(), 300); + assert_eq!(module_state.locks[1].amount.decimals(), 4); +} + +/// Test query token account info reports zero available balance when all funds are locked. +#[test] +fn test_query_token_account_info_available_zero_when_fully_locked() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account = context.external.create_account(); + let recipient = context.external.create_account(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId { + account_index: account.account_index().into(), + sequence_number: 1, + creation_order: 0, + }; + let lock_config = utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }; + utils::create_lock(&mut context, &mut block_state, &lock_id, lock_config); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + account.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + let module_state: TokenModuleAccountState = cbor::cbor_decode( + token_account_infos[0] + .account_state + .module_state + .as_ref() + .unwrap(), + ) + .unwrap(); + let available = module_state.available.unwrap(); + assert_eq!(available.value(), 0); + assert_eq!(available.decimals(), 4); + assert_eq!(module_state.locks.len(), 1); + assert_eq!(module_state.locks[0].lock, lock_id); + assert_eq!(module_state.locks[0].amount.value(), 1000); + assert_eq!(module_state.locks[0].amount.decimals(), 4); +} + +// Test that adding an account to a token list properly touches the account +#[test] +fn test_query_token_account_info_allow_list_no_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account = context.external.create_account(); + let token_id: TokenId = "TokenId3".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 4, + None, + ); + + let account_addr = context + .external + .account_canonical_address(account.account_index()); + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(account_addr), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: gov_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + assert_eq!(token_account_infos.len(), 1); + assert_eq!(token_account_infos[0].token_id, token_id); + assert_eq!( + token_account_infos[0].account_state.balance.amount, + RawTokenAmount::from(0) + ); + assert_eq!(token_account_infos[0].account_state.balance.decimals, 4); + let module_state: TokenModuleAccountState = cbor::cbor_decode( + token_account_infos[0] + .account_state + .module_state + .as_ref() + .unwrap(), + ) + .unwrap(); + assert_eq!(module_state.allow_list, Some(true)); + assert_eq!(module_state.deny_list, None); +} diff --git a/plt/plt-scheduler/tests/plt_token_account_state.rs b/plt/plt-scheduler/tests/plt_token_account_state.rs new file mode 100644 index 0000000000..faffd81ec2 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_account_state.rs @@ -0,0 +1,33 @@ +//! Tests for token module account state queries via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use concordium_base::protocol_level_tokens::TokenId; +use plt_block_state::entity::entity_test_stub; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test token module account state without lists enabled. +#[test] +fn test_query_token_module_account_state_default() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id, + TokenInitTestParams::default(), + 0, + None, + ); + let account = context.external.create_account(); + + let token_account_infos = block_state + .query_token_account_infos(&context, account.account_index()) + .unwrap(); + // Account has no balance and no list entries, so it does not appear in the infos + assert!(token_account_infos.is_empty()); +} diff --git a/plt/plt-scheduler/tests/plt_token_admin_role.rs b/plt/plt-scheduler/tests/plt_token_admin_role.rs new file mode 100644 index 0000000000..4de880a3fa --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_admin_role.rs @@ -0,0 +1,984 @@ +//! Tests for token RBAC admin role operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, RawCbor, TokenAdminRole, TokenAuthorizations, TokenId, TokenOperation, + TokenOperationsPayload, TokenPauseDetails, TokenUpdateAdminRolesDetails, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::block_state::p10::BlockStateP10; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::execution::TransactionOutcome; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// The governance account receives every role except for disabled features. +#[test] +fn test_rbac_initial_governance_account_have_every_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().deny_list(), + 2, + None, + ); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let gov = context + .external + .account_canonical_address(gov_account.account_index()); + assert!( + auth.update_admin_roles + .unwrap() + .accounts + .contains(&gov.into()) + ); + assert!(auth.mint.unwrap().accounts.contains(&gov.into())); + assert!(auth.burn.is_none()); + assert!(auth.update_allow_list.is_none()); + assert!( + auth.update_deny_list + .unwrap() + .accounts + .contains(&gov.into()) + ); + assert!(auth.pause.unwrap().accounts.contains(&gov.into())); + assert!(auth.update_metadata.unwrap().accounts.contains(&gov.into())); +} + +/// Assign multiple roles to a new account succeeds. +#[test] +fn test_rbac_assign_roles() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Assign mint and burn to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let gov = context + .external + .account_canonical_address(gov_account.account_index()); + assert!( + auth.update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&gov.into()) + ); + assert!(auth.mint.as_ref().unwrap().accounts.contains(&gov.into())); + assert!(auth.burn.as_ref().unwrap().accounts.contains(&gov.into())); + assert!(auth.update_allow_list.is_none()); + assert!(auth.update_deny_list.is_none()); + assert!(auth.pause.as_ref().unwrap().accounts.contains(&gov.into())); + assert!( + auth.update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&gov.into()) + ); + + let acc = context + .external + .account_canonical_address(account2.account_index()) + .into(); + assert!( + !auth + .update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); + assert!(auth.mint.as_ref().unwrap().accounts.contains(&acc)); + assert!(auth.burn.as_ref().unwrap().accounts.contains(&acc)); + assert!(auth.update_allow_list.is_none()); + assert!(auth.update_deny_list.is_none()); + assert!(!auth.pause.as_ref().unwrap().accounts.contains(&acc)); + assert!( + !auth + .update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); +} + +/// Assign the same role to the same account twice succeeds. +#[test] +fn test_rbac_assign_same_roles() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Assign mint and burn to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Assign mint to account2 again. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let acc = context + .external + .account_canonical_address(account2.account_index()) + .into(); + assert!( + !auth + .update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); + assert!(auth.mint.as_ref().unwrap().accounts.contains(&acc)); + assert!(auth.burn.as_ref().unwrap().accounts.contains(&acc)); + assert!(!auth.pause.as_ref().unwrap().accounts.contains(&acc)); + assert!( + !auth + .update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); +} + +/// Assign rejects when not holding the admin role. +#[test] +fn test_rbac_assign_unauthorization_sender_rejects() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Assign fails on P10 (RBAC not supported). +#[test] +fn test_rbac_assign_rejects_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Assign succeeds when paused. +#[test] +fn test_rbac_assign_role_works_when_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Pause the token. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Pause( + TokenPauseDetails {}, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Assign mint and burn to account2 while paused. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let acc = context + .external + .account_canonical_address(account2.account_index()) + .into(); + assert!( + !auth + .update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); + assert!(auth.mint.as_ref().unwrap().accounts.contains(&acc)); + assert!(auth.burn.as_ref().unwrap().accounts.contains(&acc)); + assert!(!auth.pause.as_ref().unwrap().accounts.contains(&acc)); +} + +/// Assign rejects when using a role for a feature which is not enabled. +#[test] +fn test_rbac_assign_rejects_for_unabled_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), // burn not enabled + 2, + None, + ); + let account2 = context.external.create_account(); + + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Revoke roles from the governance account. +#[test] +fn test_rbac_revoke_roles() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default() + .mintable() + .burnable() + .allow_list(), + 2, + None, + ); + + // Revoke mint and burn from gov_account. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let gov = context + .external + .account_canonical_address(gov_account.account_index()) + .into(); + assert!(auth.update_admin_roles.unwrap().accounts.contains(&gov)); + assert!(!auth.mint.as_ref().unwrap().accounts.contains(&gov)); + assert!(!auth.burn.as_ref().unwrap().accounts.contains(&gov)); + assert!(auth.update_allow_list.unwrap().accounts.contains(&gov)); + assert!(auth.update_deny_list.is_none()); // Not enabled. + assert!(auth.pause.as_ref().unwrap().accounts.contains(&gov)); + assert!( + auth.update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&gov) + ); +} + +/// Revoke the same role from the same account twice succeeds. +#[test] +fn test_rbac_revoke_same_roles() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default() + .mintable() + .burnable() + .allow_list(), + 2, + None, + ); + + // Revoke mint and burn. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Revoke mint again. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let gov = context + .external + .account_canonical_address(gov_account.account_index()) + .into(); + assert!( + auth.update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&gov) + ); + assert!(!auth.mint.as_ref().unwrap().accounts.contains(&gov)); + assert!(!auth.burn.as_ref().unwrap().accounts.contains(&gov)); + assert!( + auth.update_allow_list + .as_ref() + .unwrap() + .accounts + .contains(&gov) + ); + assert!(auth.pause.as_ref().unwrap().accounts.contains(&gov)); + assert!( + auth.update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&gov) + ); +} + +/// Revoke rejects when not holding the admin role. +#[test] +fn test_rbac_revoke_rejects_without_admin_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // account2 tries to revoke from gov — no admin role. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Revoke fails on P10 (RBAC not supported). +#[test] +fn test_rbac_revoke_rejects_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Revoke succeeds when paused. +#[test] +fn test_rbac_revoke_role_works_when_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + + // Pause the token. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Pause( + TokenPauseDetails {}, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Revoke mint and burn while paused. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + + let acc = context + .external + .account_canonical_address(gov_account.account_index()) + .into(); + assert!( + auth.update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); + assert!(!auth.mint.as_ref().unwrap().accounts.contains(&acc)); + assert!(!auth.burn.as_ref().unwrap().accounts.contains(&acc)); + assert!(auth.pause.as_ref().unwrap().accounts.contains(&acc)); + assert!( + auth.update_metadata + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); +} + +/// Revoke rejects when revoking the admin role from the sender themselves. +#[test] +fn test_rbac_revoke_admin_role_from_sender_rejects() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAdminRoles], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Revoke rejects when using a role for a feature which is not enabled. +#[test] +fn test_rbac_revoke_rejects_for_unabled_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), // burn not enabled + 2, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint, TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Rejected(_)); +} + +/// Admin role rotation: assign admin role to account2, then account2 revokes gov's admin role. +#[test] +fn test_rbac_admin_role_rotation_succeeds() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable().burnable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Assign updateAdminRoles to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAdminRoles], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // account2 revokes updateAdminRoles from gov. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAdminRoles], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let auth: TokenAuthorizations = cbor::cbor_decode( + &block_state + .query_token_authorizations(&context, &token_id) + .unwrap() + .details, + ) + .unwrap(); + let gov_acc = context + .external + .account_canonical_address(gov_account.account_index()) + .into(); + assert!( + !auth + .update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&gov_acc) + ); + let acc = context + .external + .account_canonical_address(account2.account_index()) + .into(); + assert!( + auth.update_admin_roles + .as_ref() + .unwrap() + .accounts + .contains(&acc) + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_burn.rs b/plt/plt-scheduler/tests/plt_token_burn.rs new file mode 100644 index 0000000000..91cc3781f9 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_burn.rs @@ -0,0 +1,617 @@ +//! Tests for token burn operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, DeserializationFailureRejectReason, OperationNotPermittedRejectReason, + RawCbor, TokenAdminRole, TokenAmount, TokenId, TokenModuleRejectReason, TokenOperation, + TokenOperationsPayload, TokenSupplyUpdateDetails, TokenUpdateAdminRolesDetails, + UnsupportedOperationRejectReason, +}; +use concordium_base::transactions::Payload; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test successful burns. +#[test] +fn test_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + Some(RawTokenAmount::from(5000)), + ); + + // First burn + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + + // Second burn + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(2000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); +} + +/// Rejects burn operations from non-governance accounts. +/// The governance check is performed before the burnable feature check. +#[test] +fn test_unauthorized_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + None, + ); + let non_governance_account = context.external.create_account(); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let non_gov_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_gov_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + .. + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from( + context.external.account_canonical_address(non_governance_account.account_index()) + )) + ); + } + ); +} + +/// Test burn amount that exceeds account balance. +#[test] +fn test_burn_insufficient_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + Some(RawTokenAmount::from(1000)), + ); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(2000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::TokenBalanceInsufficient( + concordium_base::protocol_level_tokens::TokenBalanceInsufficientRejectReason { + available_balance, + required_balance, + .. + }) => { + assert_eq!(required_balance, TokenAmount::from_raw(2000, 2)); + assert_eq!(available_balance, TokenAmount::from_raw(1000, 2)); + }); +} + +#[test] +fn test_burn_insufficient_available_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + Some(RawTokenAmount::from(1000)), + ); + let recipient = context.external.create_account(); + + let lock_id = LockId::new(gov_account.account_index(), 7u64, 0); + utils::create_lock( + &mut context, + &mut block_state, + &lock_id, + utils::CreateLockSimpleConfig { + recipients: vec![recipient.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: gov_account.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }, + ); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(250), + ); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(800, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::TokenBalanceInsufficient( + concordium_base::protocol_level_tokens::TokenBalanceInsufficientRejectReason { + available_balance, + required_balance, + .. + }) => { + assert_eq!(required_balance, TokenAmount::from_raw(800, 2)); + assert_eq!(available_balance, TokenAmount::from_raw(750, 2)); + }); +} + +/// Test burn with amount specified with wrong number of decimals. +#[test] +fn test_burn_decimals_mismatch() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::DeserializationFailure( + DeserializationFailureRejectReason { + cause: Some(cause) + }) => { + assert!(cause.contains("decimals mismatch"), "cause: {}", cause); + }); +} + +/// Reject "burn" operations while token is paused. +#[test] +fn test_burn_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable().mintable(), + 2, + Some(RawTokenAmount::from(5000)), + ); + + utils::pause_token( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + ); + + // Now attempt to burn while paused + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: None, + reason: Some(reason), + }) if reason == "token operation burn is paused" + ); +} + +/// Reject "burn" operation if the feature is not enabled. +#[test] +fn test_not_burnable() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason), + }) if reason == "feature not enabled" && operation_type == "burn" + ); +} + +/// Test that burn events contain expected data. +#[test] +fn test_burn_event() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + Some(RawTokenAmount::from(5000)), + ); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenBurn(burn) => { + assert_eq!(burn.token_id, token_id); + assert_eq!(burn.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(burn.amount.decimals, 2); + assert_eq!(burn.target, TokenHolder::Account(context.external.account_canonical_address(gov_account.account_index()))); + }); +} + +/// Rejects burn when governance account does not hold the burn role. +#[test] +fn test_role_authorization_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 2, + None, + ); + + // Revoke burn role from governance account. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Burn], + account: CborHolderAccount::from(gov_account_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to burn as governance account (no longer has burn role). + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Burn( + TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(200, 2), + }, + )])), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(context.external.account_canonical_address(gov_account.account_index()))); + } + ); +} + +/// Succeeds for another account holding the burn role. +#[test] +fn test_new_account_with_role_succeeds_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable().mintable(), + 2, + None, + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(10000), + ); + let account2 = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + account2.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + // Assign burn role to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Burn], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Burn as account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Burn( + TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(200, 2), + }, + )])), + }; + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(10000) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(4800) + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_general_transactions.rs b/plt/plt-scheduler/tests/plt_token_general_transactions.rs new file mode 100644 index 0000000000..2dccc07612 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_general_transactions.rs @@ -0,0 +1,390 @@ +//! General tests for token update transactions via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::{ + AddressNotFoundRejectReason, CborHolderAccount, DeserializationFailureRejectReason, RawCbor, + TokenAmount, TokenId, TokenModuleRejectReason, TokenOperation, TokenOperationsPayload, + TokenTransfer, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +use crate::utils::BlockStateLatest; + +mod utils; + +const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); + +/// Test failure to decode token operations. +#[test] +fn test_update_token_decode_failure() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(vec![]), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::DeserializationFailure( + DeserializationFailureRejectReason { cause: Some(cause) }) => { + assert!(cause.contains("IO error"), "cause: {}", cause); + }); +} + +/// Test additional fields specified in token update operation. +#[test] +fn test_update_token_additional_fields() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + let receiver = context.external.create_account(); + + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + })]; + + let mut dynamic_operations: cbor::value::Value = + cbor::cbor_decode(cbor::cbor_encode(&operations)).unwrap(); + let operations_array = + assert_matches!(&mut dynamic_operations, cbor::value::Value::Array(array) => array); + let operation0_outer_map = + assert_matches!(&mut operations_array[0], cbor::value::Value::Map(map) => map); + let operation0_map = + assert_matches!(&mut operation0_outer_map[0].1, cbor::value::Value::Map(map) => map); + operation0_map.push(( + cbor::value::Value::Text("additionalField".to_string()), + cbor::value::Value::Text("testvalue1".to_string()), + )); + + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&dynamic_operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::DeserializationFailure( + DeserializationFailureRejectReason { cause: Some(cause) }) => { + assert!(cause.contains("unknown map key"), "cause: {}", cause); + }); +} + +/// Test transaction with multiple operations. +#[test] +fn test_multiple_operations() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (_, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + receiver.account_index(), + &token_id, + RawTokenAmount::from(2000), + ); + + let operations = vec![ + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + }), + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(2000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); +} + +/// Test transaction with multiple operations where one of them fails. +/// The failing operation is placed first so no state changes occur on rejection. +#[test] +fn test_single_failing_operation() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let operations = vec![ + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(2000, 2), + recipient: CborHolderAccount::from(NON_EXISTING_ACCOUNT), + memo: None, + }), + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::AddressNotFound( + AddressNotFoundRejectReason { index, address }) => { + assert_eq!(address.address, NON_EXISTING_ACCOUNT); + assert_eq!(index, 0); + }); +} + +/// Test that energy is charged for execution of operations. +#[test] +fn test_energy_charge() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(1000), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + // 300 (lookup) + 100 (1 operation) = 400 total energy used. + assert_eq!(result.energy_used.energy, 400); +} + +/// Test hitting out of energy error. +#[test] +fn test_out_of_energy_error() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from( + context + .external + .account_canonical_address(receiver.account_index()), + ), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + // 50 energy is less than the 300 lookup cost. + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(50), + sender_account_address: sender_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + assert_matches!( + result.outcome, + TransactionOutcome::Rejected(TransactionRejectReason::OutOfEnergy) + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_initialize.rs b/plt/plt-scheduler/tests/plt_token_initialize.rs new file mode 100644 index 0000000000..7faab1c566 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_initialize.rs @@ -0,0 +1,451 @@ +//! Tests for token initialization via the scheduler. + +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, MetadataUrl, TokenAmount, TokenId, TokenModuleInitializationParameters, + TokenModuleState, +}; +use concordium_base::updates::{CreatePlt, UpdatePayload}; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, FailureKind}; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +use crate::utils::BlockStateLatest; +use crate::utils::entity_traits::scheduler::SchedulerOperations; + +mod utils; + +const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); + +/// In this example, the parameters are not a valid encoding. +#[test] +fn test_initialize_token_parameters_decode_failure() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: vec![].into(), + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("IO error"), "err: {}", err); + } + ); +} + +/// In this example, a parameter is missing from the required initialization parameters. +#[test] +fn test_initialize_token_parameters_missing() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let parameters = TokenModuleInitializationParameters { + name: None, + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some( + context + .external + .account_canonical_address(gov_account.account_index()) + .into(), + ), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) if err.contains("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 context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some( + context + .external + .account_canonical_address(gov_account.account_index()) + .into(), + ), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + }; + + let mut dynamic_parameters: cbor::value::Value = + cbor::cbor_decode(cbor::cbor_encode(¶meters)).unwrap(); + assert_matches!(&mut dynamic_parameters, cbor::value::Value::Map(map) => { + map.push((cbor::value::Value::Text("additionalField".to_string()), cbor::value::Value::Text("testvalue1".to_string()))); + }); + + let encoded_parameters = cbor::cbor_encode(&dynamic_parameters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("unknown map key"), "err: {}", err); + } + ); +} + +/// In this example, minimal parameters are specified to check defaulting behaviour. +#[test] +fn test_initialize_token_default_values() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: None, + deny_list: None, + initial_supply: None, + mintable: None, + burnable: None, + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + + // Assertions using token module state query + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(state.name, Some("Protocol-level token".to_owned())); + assert_eq!(state.metadata, Some(metadata)); + assert_eq!(state.governance_account, Some(gov_holder_account)); + assert_eq!(state.allow_list, Some(false)); + assert_eq!(state.deny_list, Some(false)); + assert_eq!(state.mintable, Some(false)); + assert_eq!(state.burnable, Some(false)); + assert_eq!(state.paused, Some(false)); + + // Assert governance account balance + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + assert_eq!( + gov_account.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(0) + ); +} + +/// In this example, the parameters are valid, no minting. +#[test] +fn test_initialize_token_no_minting() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: Some(true), + deny_list: Some(false), + initial_supply: None, + mintable: Some(true), + burnable: Some(true), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + + // Assertions using token module state query + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(state.name, Some("Protocol-level token".to_owned())); + assert_eq!(state.metadata, Some(metadata)); + assert_eq!(state.governance_account, Some(gov_holder_account)); + assert_eq!(state.allow_list, Some(true)); + assert_eq!(state.deny_list, Some(false)); + assert_eq!(state.mintable, Some(true)); + assert_eq!(state.burnable, Some(true)); + assert_eq!(state.paused, Some(false)); + + // Assert governance account balance + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + assert_eq!( + gov_account.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(0) + ); +} + +/// In this example, the parameters are valid, with minting. +#[test] +fn test_initialize_token_with_minting() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: Some(false), + deny_list: Some(true), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals: 2, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + + // Assertions using token module state query + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(state.name, Some("Protocol-level token".to_owned())); + assert_eq!(state.metadata, Some(metadata)); + assert_eq!(state.governance_account, Some(gov_holder_account)); + assert_eq!(state.allow_list, Some(false)); + assert_eq!(state.deny_list, Some(true)); + assert_eq!(state.mintable, Some(false)); + assert_eq!(state.burnable, Some(false)); + assert_eq!(state.paused, Some(false)); + + // Assert governance account balance and circulating supply + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + assert_eq!( + gov_account.account_token_balance(&context, token.token_p9_base.token_index()), + RawTokenAmount::from(500000) + ); + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(500000) + ); +} + +/// 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 context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some( + context + .external + .account_canonical_address(gov_account.account_index()) + .into(), + ), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 6)), + mintable: Some(false), + burnable: Some(false), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 2, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("decimals mismatch"), "err: {}", err); + } + ); +} + +/// 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 context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let gov_account = context.external.create_account(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some( + context + .external + .account_canonical_address(gov_account.account_index()) + .into(), + ), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 6, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("decimals mismatch"), "err: {}", err); + } + ); +} + +/// In this example, the parameters specify a non-existing governance account. +#[test] +fn test_initialize_token_non_existing_governance_account() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some("https://plt.token".to_owned().into()), + governance_account: Some(NON_EXISTING_ACCOUNT.into()), + allow_list: Some(false), + deny_list: Some(false), + initial_supply: Some(TokenAmount::from_raw(500000, 2)), + mintable: Some(false), + burnable: Some(false), + }; + let encoded_parameters = cbor::cbor_encode(¶meters).into(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let outcome = block_state + .execute_chain_update( + &mut context, + UpdatePayload::CreatePlt(CreatePlt { + token_id, + token_module: TOKEN_MODULE_REF, + decimals: 0, + initialization_parameters: encoded_parameters, + }), + ) + .unwrap(); + let failure_kind = + assert_matches!(outcome, ChainUpdateOutcome::Failed(failure_kind) => failure_kind); + assert_matches!( + failure_kind, + FailureKind::TokenInitializeFailure(err) => { + assert!(err.contains("does not exist"), "err: {}", err); + } + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_list_transactions.rs b/plt/plt-scheduler/tests/plt_token_list_transactions.rs new file mode 100644 index 0000000000..d1a0e5b8e8 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_list_transactions.rs @@ -0,0 +1,1247 @@ +//! Tests for token allow/deny list update operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, OperationNotPermittedRejectReason, RawCbor, TokenAdminRole, TokenId, + TokenListUpdateDetails, TokenListUpdateEventDetails, TokenModuleAccountState, + TokenModuleEventType, TokenModuleRejectReason, TokenOperation, TokenOperationsPayload, + TokenUpdateAdminRolesDetails, UnsupportedOperationRejectReason, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test allow list add then remove. +#[test] +fn test_allow_list_updates() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + // Target account not yet touched + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + + // Add to allow list + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::AddAllowList.to_type_discriminator()); + let add_event: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(add_event.target, CborHolderAccount::from(target_addr)); + }); + let infos = block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.allow_list, Some(true)); + assert_eq!(state.deny_list, None); + + // Remove from allow list + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::RemoveAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::RemoveAllowList.to_type_discriminator()); + let remove_event: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(remove_event.target, CborHolderAccount::from(target_addr)); + }); + let infos = block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.allow_list, Some(false)); + assert_eq!(state.deny_list, None); +} + +/// Test deny list add then remove. +#[test] +fn test_deny_list_updates() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + // Target account not yet touched + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + + // Add to deny list + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::AddDenyList.to_type_discriminator()); + let add_event: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(add_event.target, CborHolderAccount::from(target_addr)); + }); + let infos = block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.allow_list, None); + assert_eq!(state.deny_list, Some(true)); + + // Remove from deny list + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::RemoveDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::RemoveDenyList.to_type_discriminator()); + let remove_event: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(remove_event.target, CborHolderAccount::from(target_addr)); + }); + let infos = block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.allow_list, None); + assert_eq!(state.deny_list, Some(false)); +} + +/// Non-governance account cannot add to allow list. +#[test] +fn test_add_allow_list_reject_non_governance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let sender = context.external.create_account(); + let target_account = context.external.create_account(); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address: Some(address), + reason: Some(reason), + .. + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + } + ); + + // Target account must remain untouched + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// Non-governance account cannot remove from allow list. +#[test] +fn test_remove_allow_list_reject_non_governance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let sender = context.external.create_account(); + let target_account = context.external.create_account(); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address: Some(address), + reason: Some(reason), + .. + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + } + ); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// Non-governance account cannot add to deny list. +#[test] +fn test_add_deny_list_reject_non_governance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let sender = context.external.create_account(); + let target_account = context.external.create_account(); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address: Some(address), + reason: Some(reason), + .. + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + } + ); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// Non-governance account cannot remove from deny list. +#[test] +fn test_remove_deny_list_reject_non_governance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let sender = context.external.create_account(); + let target_account = context.external.create_account(); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address: Some(address), + reason: Some(reason), + .. + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + } + ); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// AddAllowList touches the target account. +#[test] +fn test_add_allow_list_touches_account() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + + assert!( + !block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// RemoveAllowList touches the target account. +#[test] +fn test_remove_allow_list_touches_account() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::RemoveAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + + assert!( + !block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// AddDenyList touches the target account. +#[test] +fn test_add_deny_list_touches_account() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + + assert!( + !block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// RemoveDenyList touches the target account. +#[test] +fn test_remove_deny_list_touches_account() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let target_account = context.external.create_account(); + + assert!( + block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); + + let target_addr = context + .external + .account_canonical_address(target_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::RemoveDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(target_addr), + })], + ); + + assert!( + !block_state + .query_token_account_infos(&context, target_account.account_index()) + .unwrap() + .is_empty() + ); +} + +/// Adding to allow list fails when the allow list feature is not enabled. +#[test] +fn test_add_to_not_enabled_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), // allow_list not enabled + 0, + None, + ); + let allow_account = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let allow_addr = context + .external + .account_canonical_address(allow_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(allow_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason), + }) if reason == "feature not enabled" && operation_type == "addAllowList" + ); +} + +/// Removing from allow list fails when the allow list feature is not enabled. +#[test] +fn test_remove_from_not_enabled_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), // allow_list not enabled + 0, + None, + ); + let allow_account = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let allow_addr = context + .external + .account_canonical_address(allow_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(allow_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason), + }) if reason == "feature not enabled" && operation_type == "removeAllowList" + ); +} + +/// Adding to deny list fails when the deny list feature is not enabled. +#[test] +fn test_add_to_not_enabled_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), // deny_list not enabled + 0, + None, + ); + let deny_account = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let deny_addr = context + .external + .account_canonical_address(deny_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(deny_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason), + }) if reason == "feature not enabled" && operation_type == "addDenyList" + ); +} + +/// Removing from deny list fails when the deny list feature is not enabled. +#[test] +fn test_remove_from_not_enabled_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), // deny_list not enabled + 0, + None, + ); + let deny_account = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let deny_addr = context + .external + .account_canonical_address(deny_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(deny_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason), + }) if reason == "feature not enabled" && operation_type == "removeDenyList" + ); +} + +/// Rejects AddDenyList when governance account does not hold the updateDenyList role. +#[test] +fn test_reject_add_denylist_without_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // Revoke updateDenyList role from governance account. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateDenyList], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to add to deny list as governance account. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(gov_addr)); + } + ); +} + +/// Rejects AddAllowList when governance account does not hold the updateAllowList role. +#[test] +fn test_reject_add_allowlist_without_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // Revoke updateAllowList role. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAllowList], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to add to allow list. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(gov_addr)); + } + ); +} + +/// Rejects RemoveDenyList when governance account does not hold the updateDenyList role. +#[test] +fn test_reject_remove_denylist_without_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // First add gov to deny list, then revoke the role. + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + })], + ); + + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateDenyList], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to remove from deny list. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(gov_addr)); + } + ); +} + +/// Rejects RemoveAllowList when governance account does not hold the updateAllowList role. +#[test] +fn test_reject_remove_allowlist_without_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // First add gov to allow list, then revoke the role. + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + })], + ); + + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAllowList], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to remove from allow list. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RemoveAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(gov_addr)); + } + ); +} + +/// Succeeds for another account holding the updateDenyList role. +#[test] +fn test_succeeds_add_deny_list_new_account_with_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list(), + 0, + None, + ); + let account2 = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + + // Assign updateDenyList role to account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateDenyList], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Add gov to deny list as account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddDenyList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let infos = block_state + .query_token_account_infos(&context, gov_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.deny_list, Some(true)); +} + +/// Succeeds for another account holding the updateAllowList role. +#[test] +fn test_succeeds_add_allow_list_new_account_with_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 0, + None, + ); + let account2 = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + + // Assign updateAllowList role to account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateAllowList], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Add gov to allow list as account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AddAllowList( + TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let infos = block_state + .query_token_account_infos(&context, gov_account.account_index()) + .unwrap(); + let module_state = infos[0].account_state.module_state.as_ref().unwrap(); + let state: TokenModuleAccountState = cbor::cbor_decode(module_state).unwrap(); + assert_eq!(state.allow_list, Some(true)); +} diff --git a/plt/plt-scheduler/tests/plt_token_metadata_transactions.rs b/plt/plt-scheduler/tests/plt_token_metadata_transactions.rs new file mode 100644 index 0000000000..f6059c9f54 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_metadata_transactions.rs @@ -0,0 +1,273 @@ +//! Tests for token metadata update operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::{self, cbor}; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, MetadataUrl, OperationNotPermittedRejectReason, RawCbor, TokenAdminRole, + TokenId, TokenModuleRejectReason, TokenModuleState, TokenOperation, TokenOperationsPayload, + TokenUpdateAdminRolesDetails, UnsupportedOperationRejectReason, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::execution::TransactionOutcome; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Succeeds in setting the token metadata. +#[test] +fn test_token_metadata_updates() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + // Check initial metadata via query. + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let initial_state: TokenModuleState = + cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!( + initial_state.metadata, + Some(MetadataUrl::from("https://plt.token".to_string())) + ); + + let new_metadata_url = MetadataUrl { + url: "https://plt2.token".to_string(), + checksum_sha_256: Some([5u8; 32].into()), + additional: Default::default(), + }; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::UpdateMetadata( + new_metadata_url.clone(), + )])), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let module_state: TokenModuleState = cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(module_state.metadata.unwrap(), new_metadata_url); +} + +/// Succeeds for another account holding the updateMetadata role. +#[test] +fn test_new_account_with_role_succeeds_update_metadata() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Assign the updateMetadata role to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateMetadata], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let new_metadata_url = MetadataUrl { + url: "https://plt2.token".to_string(), + checksum_sha_256: Some([5u8; 32].into()), + additional: Default::default(), + }; + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::UpdateMetadata( + new_metadata_url.clone(), + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let module_state: TokenModuleState = cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(module_state.metadata.unwrap(), new_metadata_url); +} + +/// Reject when governance account is not holding the updateMetadata role. +#[test] +fn test_role_authorization_update_metadata() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + // Remove updateMetadata role from governance account. + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::UpdateMetadata], + account: CborHolderAccount::from(gov_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let new_metadata_url = MetadataUrl { + url: "https://plt2.token".to_string(), + checksum_sha_256: Some([5u8; 32].into()), + additional: Default::default(), + }; + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::UpdateMetadata( + new_metadata_url, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason) + }) => { + assert_eq!(&reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(context.external.account_canonical_address(gov_account.account_index()))); + } + ); +} + +/// Reject when additional metadata fields are provided. +#[test] +fn test_update_metadata_rejects_with_additional_data() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let new_metadata_url = MetadataUrl { + url: "https://plt2.token".to_string(), + checksum_sha_256: Some([5u8; 32].into()), + additional: [( + "my_own_data_field".to_string(), + common::cbor::value::Value::Text("custom_data".to_string()), + )] + .into(), + }; + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::UpdateMetadata( + new_metadata_url, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + reason: Some(reason), + operation_type + }) => { + assert_eq!(&operation_type, "updateMetadata"); + assert_eq!(&reason, "Unknown additional metadata fields"); + } + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_mint.rs b/plt/plt-scheduler/tests/plt_token_mint.rs new file mode 100644 index 0000000000..a171609237 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_mint.rs @@ -0,0 +1,751 @@ +//! Tests for token mint operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, DeserializationFailureRejectReason, MintWouldOverflowRejectReason, + OperationNotPermittedRejectReason, RawCbor, TokenAdminRole, TokenAmount, TokenId, + TokenModuleRejectReason, TokenOperation, TokenOperationsPayload, TokenSupplyUpdateDetails, + TokenUpdateAdminRolesDetails, UnsupportedOperationRejectReason, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::block_state::p10::BlockStateP10; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +mod utils; + +use crate::utils::BlockStateLatest; + +/// Test successful mints on P10. +#[test] +fn test_mint_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + // First mint + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_address = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_address), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Second mint + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(4000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_address, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); +} + +/// Rejects mint operations from non-governance accounts on protocol version 10. +#[test] +fn test_unauthorized_mint_p10() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateP10::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p9( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let non_governance_account = context.external.create_account(); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let non_governance_account_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_governance_account_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + .. + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from(non_governance_account_addr)) + ); + } + ); + + // Assert balances remain unchanged. + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + assert_eq!( + non_governance_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); +} + +/// Test successful mints. +#[test] +fn test_mint() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + // First mint + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Second mint + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(4000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); +} + +/// Rejects mint operations from non-governance accounts. +#[test] +fn test_unauthorized_mint() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let non_governance_account = context.external.create_account(); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let non_governance_account_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_governance_account_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + .. + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from(non_governance_account_addr)) + ); + } + ); + + // Assert balances remain unchanged. + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + assert_eq!( + non_governance_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); +} + +/// Rejects mint operations from non-governance accounts using alias address. +/// Check that address in reject reason is the alias and not the canonical address. +#[test] +fn test_unauthorized_mint_using_alias() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let non_gov_account = context.external.create_account(); + let non_gov_account_address_alias = context + .external + .account_canonical_address(non_gov_account.account_index()) + .get_alias(5) + .unwrap(); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_gov_account_address_alias), + non_gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address, + .. + }) => { + // Assert the address alias is used in the reject reason. + assert_eq!( + address, + Some(CborHolderAccount::from(non_gov_account_address_alias)) + ); + } + ); +} + +/// Test mint that would overflow circulating supply. +#[test] +fn test_mint_overflow() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + Some(RawTokenAmount::from(1000)), + ); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw( + RawTokenAmount::MAX + .checked_sub(RawTokenAmount::from(500)) + .unwrap() + .into(), + 2, + ), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::MintWouldOverflow( + MintWouldOverflowRejectReason { + requested_amount, + current_supply, + max_representable_amount, + .. + }) => { + assert_eq!(requested_amount, TokenAmount::from_raw(RawTokenAmount::MAX.checked_sub(RawTokenAmount::from(500)).unwrap().into(), 2)); + assert_eq!(current_supply, TokenAmount::from_raw(1000, 2)); + assert_eq!(max_representable_amount, TokenAmount::from_raw(RawTokenAmount::MAX.into(), 2)); + }); + + // Supply unchanged + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(1000) + ); +} + +/// Test mint with initial supply specified with wrong number of decimals. +#[test] +fn test_mint_decimals_mismatch() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::DeserializationFailure( + DeserializationFailureRejectReason { + cause: Some(cause) + }) => { + assert!(cause.contains("decimals mismatch"), "cause: {}", cause); + }); +} + +/// Reject "mint" operations while token is paused. +#[test] +fn test_mint_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + // Pause the token first + utils::pause_token( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + ); + + // Now attempt to mint while paused + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: None, + reason: Some(reason), + }) if reason == "token operation mint is paused" + ); +} + +/// Reject "mint" operation if the feature is not enabled. +#[test] +fn test_not_mintable() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 2, + None, + ); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw( + RawTokenAmount::MAX + .checked_sub(RawTokenAmount::from(500)) + .unwrap() + .into(), + 2, + ), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + index: 0, + operation_type, + reason: Some(reason) + }) if reason == "feature not enabled" && operation_type == "mint" + ); +} + +/// Test that mint events contain expected data. +#[test] +fn test_mint_event() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenMint(mint) => { + assert_eq!(mint.token_id, token_id); + assert_eq!(mint.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(mint.amount.decimals, 2); + assert_eq!(mint.target, TokenHolder::Account(gov_account_addr)); + }); +} + +/// Rejects mint when governance account does not hold the mint role. +#[test] +fn test_reject_without_role() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + // Revoke mint role from governance account. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint], + account: CborHolderAccount::from(gov_account_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to mint as governance account (no longer has mint role). + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Mint( + TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(200, 2), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(gov_account_addr)); + } + ); +} + +/// Succeeds when another account holds the mint role. +#[test] +fn test_new_account_with_role_succeeds_mint() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let account2 = context.external.create_account(); + + // Assign mint role to account2. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Mint], + account: CborHolderAccount::from( + context + .external + .account_canonical_address(account2.account_index()), + ), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Mint as account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Mint( + TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(200, 2), + }, + )])), + }; + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(200) + ); +} diff --git a/plt/plt-scheduler/tests/plt_token_pause.rs b/plt/plt-scheduler/tests/plt_token_pause.rs new file mode 100644 index 0000000000..ed1bf172c9 --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_pause.rs @@ -0,0 +1,627 @@ +//! Tests for token pause/unpause operations via the scheduler. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, OperationNotPermittedRejectReason, RawCbor, TokenAdminRole, TokenAmount, + TokenId, TokenModuleEventType, TokenModuleRejectReason, TokenModuleState, TokenOperation, + TokenOperationsPayload, TokenPauseDetails, TokenPauseEventDetails, TokenSupplyUpdateDetails, + TokenUpdateAdminRolesDetails, +}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test that pause/unpause operations modify the token module state as expected. +#[test] +fn test_token_pause_state() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); + + // Pause the token + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Pause(TokenPauseDetails {})], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::Pause.to_type_discriminator()); + let _details: TokenPauseEventDetails = cbor::cbor_decode(&event.details).unwrap(); + }); + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); + + // Unpause the token + let unpause_ops = vec![TokenOperation::Unpause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&unpause_ops)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::Unpause.to_type_discriminator()); + let _details: TokenPauseEventDetails = cbor::cbor_decode(&event.details).unwrap(); + }); + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// Performing a double pause within one transaction and then again in another is permitted. +#[test] +fn test_double_pause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + // Double pause in one transaction + let operations = vec![ + TokenOperation::Pause(TokenPauseDetails {}), + TokenOperation::Pause(TokenPauseDetails {}), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + assert_eq!(events.len(), 2); + for event in &events { + assert_matches!(event, BlockItemEvent::TokenModule(e) => { + assert_eq!(e.event_type, TokenModuleEventType::Pause.to_type_discriminator()); + }); + } + + // Pause again in a subsequent transaction + let events = utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Pause(TokenPauseDetails {})], + ); + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(e) => { + assert_eq!(e.event_type, TokenModuleEventType::Pause.to_type_discriminator()); + }); + + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// Performing an unpause when the token is not paused is permitted. +#[test] +fn test_redundant_unpause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + // Token is already unpaused; unpause again + let unpause_ops = vec![TokenOperation::Unpause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&unpause_ops)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.event_type, TokenModuleEventType::Unpause.to_type_discriminator()); + }); + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// Rejects pause operations from non-governance accounts. +#[test] +fn test_unauthorized_pause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + let non_governance_account = context.external.create_account(); + + let operations = vec![TokenOperation::Pause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let non_gov_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_gov_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + .. + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from( + context.external.account_canonical_address(non_governance_account.account_index()) + )) + ); + } + ); + + // Token must remain unpaused + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// Rejects unpause operations from non-governance accounts. +#[test] +fn test_unauthorized_unpause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + let non_governance_account = context.external.create_account(); + + // Gov pauses the token first + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Pause(TokenPauseDetails {})], + ); + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); + + // Non-gov attempts to unpause + let operations = vec![TokenOperation::Unpause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let non_gov_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_gov_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + .. + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from( + context.external.account_canonical_address(non_governance_account.account_index()) + )) + ); + } + ); + + // Token must remain paused + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// A transaction [Pause, Mint] is rejected because Mint is not permitted while paused. +/// +/// Semantic note: In the scheduler, the token module key-value state is a local copy that is +/// only committed on transaction success. Therefore the Pause takes effect within the same +/// transaction's local state, but since the transaction ultimately fails (at Mint), the local +/// state is discarded. The token is NOT paused and NO events are emitted after this rejection. +#[test] +fn test_pause_multiple_ops() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .expect("created token"); + + let operations = vec![ + TokenOperation::Pause(TokenPauseDetails {}), + TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 1, + address: None, + reason: Some(reason), + }) if reason == "token operation mint is paused" + ); + + // No tokens minted + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + // Token is NOT paused (local state was discarded on rejection) + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// A transaction [Unpause, Mint] succeeds: unpause takes effect first, then mint proceeds. +#[test] +fn test_unpause_multiple_ops() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + + // Pause the token first + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::Pause(TokenPauseDetails {})], + ); + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); + + // [Unpause, Mint] in one transaction + let operations = vec![ + TokenOperation::Unpause(TokenPauseDetails {}), + TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 2), + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + // 2 events: Unpause + Mint + assert_eq!(events.len(), 2); + assert_matches!(&events[0], BlockItemEvent::TokenModule(e) => { + assert_eq!(e.event_type, TokenModuleEventType::Unpause.to_type_discriminator()); + }); + assert_matches!(&events[1], BlockItemEvent::TokenMint(_)); + + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(1000) + ); +} + +/// Rejects pause when governance account does not hold the pause role. +#[test] +fn test_role_authorization_pause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + + // Revoke pause role from governance account. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::RevokeAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Pause], + account: CborHolderAccount::from(gov_account_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Attempting to pause as governance account (no longer has pause role). + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Pause( + TokenPauseDetails {}, + )])), + }; + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, + address: Some(address), + reason: Some(reason), + }) => { + assert_eq!(reason, "sender is not authorized to perform the operation for this token"); + assert_eq!(address, CborHolderAccount::from(context.external.account_canonical_address(gov_account.account_index()))); + } + ); + // Token must remain unpaused. + assert!(!{ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} + +/// Succeeds for another account holding the pause role. +#[test] +fn test_new_account_with_role_succeeds_pause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 0, + None, + ); + let account2 = context.external.create_account(); + + // Assign pause role to account2. + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::AssignAdminRoles( + TokenUpdateAdminRolesDetails { + roles: vec![TokenAdminRole::Pause], + account: CborHolderAccount::from(account2_addr), + }, + )])), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Pause as account2. + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Pause( + TokenPauseDetails {}, + )])), + }; + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + assert!({ + let info = block_state.query_token_info(&context, &token_id).unwrap(); + let state: TokenModuleState = cbor::cbor_decode(&info.state.module_state).unwrap(); + state.paused.unwrap_or(false) + }); +} diff --git a/plt/plt-scheduler/tests/plt_token_transfer.rs b/plt/plt-scheduler/tests/plt_token_transfer.rs new file mode 100644 index 0000000000..faad89d44f --- /dev/null +++ b/plt/plt-scheduler/tests/plt_token_transfer.rs @@ -0,0 +1,994 @@ +//! Tests for token transfer operations via the scheduler. + +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use crate::utils::{BlockStateLatest, TokenInitTestParams}; +use assert_matches::assert_matches; +use concordium_base::common::cbor; +use concordium_base::contracts_common::AccountAddress; +use concordium_base::protocol_level_locks::{LockControllerSimpleV0Capability, LockId}; +use concordium_base::protocol_level_tokens::{ + AddressNotFoundRejectReason, CborHolderAccount, CborMemo, DeserializationFailureRejectReason, + OperationNotPermittedRejectReason, RawCbor, TokenAmount, TokenBalanceInsufficientRejectReason, + TokenId, TokenListUpdateDetails, TokenModuleRejectReason, TokenOperation, + TokenOperationsPayload, TokenPauseDetails, TokenTransfer, +}; +use concordium_base::transactions::{Memo, Payload}; +use plt_block_state::{ + entity::entity_test_stub, persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant, +}; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +mod utils; + +const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); + +#[test] +fn test_transfer() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (_, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + receiver.account_index(), + &token_id, + RawTokenAmount::from(2000), + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(3000) + ); +} + +#[test] +fn test_transfer_with_memo() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (_, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let memo = CborMemo::Cbor(Memo::try_from(cbor::cbor_encode("testvalue")).unwrap()); + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: Some(memo), + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); +} + +#[test] +fn test_transfer_self() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (_, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(sender_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); +} + +#[test] +fn test_transfer_insufficient_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(10000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::TokenBalanceInsufficient( + TokenBalanceInsufficientRejectReason { available_balance, required_balance, .. }) => { + assert_eq!(available_balance, TokenAmount::from_raw(5000, 2)); + assert_eq!(required_balance, TokenAmount::from_raw(10000, 2)); + }); +} + +#[test] +fn test_transfer_insufficient_available_balance() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(1000), + ); + + let lock_id = LockId::new(sender.account_index(), 7u64, 0); + utils::create_lock( + &mut context, + &mut block_state, + &lock_id, + utils::CreateLockSimpleConfig { + recipients: vec![receiver.account_index()], + grants: vec![LockControllerSimpleV0Grant { + account: sender.account_index(), + roles: vec![LockControllerSimpleV0Capability::Fund], + }], + tokens: vec![token_id.clone()], + expiry: 1_804_806_000, + keep_alive: false, + }, + ); + utils::lock_balance( + &mut context, + &mut block_state, + &lock_id, + sender.account_index(), + &token_id, + RawTokenAmount::from(250), + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(800, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::TokenBalanceInsufficient( + TokenBalanceInsufficientRejectReason { available_balance, required_balance, .. }) => { + assert_eq!(available_balance, TokenAmount::from_raw(750, 2)); + assert_eq!(required_balance, TokenAmount::from_raw(800, 2)); + }); +} + +#[test] +fn test_transfer_decimals_mismatch() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::DeserializationFailure( + DeserializationFailureRejectReason { cause: Some(cause) }) => { + assert!(cause.contains("decimals mismatch"), "cause: {}", cause); + }); +} + +#[test] +fn test_transfer_to_non_existing_receiver() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(NON_EXISTING_ACCOUNT), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::AddressNotFound( + AddressNotFoundRejectReason { address, .. }) => { + assert_eq!(address.address, NON_EXISTING_ACCOUNT); + }); +} + +#[test] +fn test_transfer_allow_list_success() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 2, + Some(RawTokenAmount::from(5000)), + ); + let receiver = context.external.create_account(); + + // Gov is NOT auto-added to allow list — must add explicitly. + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_account_addr), + })], + ); + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(receiver_addr), + })], + ); + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); +} + +#[test] +fn test_transfer_deny_list_success() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + let denied = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + receiver.account_index(), + &token_id, + RawTokenAmount::from(2000), + ); + + let denied_addr = context + .external + .account_canonical_address(denied.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(denied_addr), + })], + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(3000) + ); +} + +#[test] +fn test_transfer_sender_not_in_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 2, + Some(RawTokenAmount::from(5000)), + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(receiver_addr), + })], + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, address: Some(address), reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender not in allow list"); + } + ); +} + +#[test] +fn test_transfer_recipient_not_in_allow_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 2, + Some(RawTokenAmount::from(5000)), + ); + let receiver = context.external.create_account(); + + // Gov is NOT auto-added to allow list — add gov so it can transfer, + // but do NOT add receiver — transfer should fail with "recipient not in allow list". + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_account_addr), + })], + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, address: Some(address), reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(receiver_addr)); + assert_eq!(reason, "recipient not in allow list"); + } + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); +} + +#[test] +fn test_transfer_sender_in_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + receiver.account_index(), + &token_id, + RawTokenAmount::from(2000), + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(sender_addr), + })], + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, address: Some(address), reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(sender_addr)); + assert_eq!(reason, "sender in deny list"); + } + ); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); +} + +#[test] +fn test_transfer_recipient_in_deny_list() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().deny_list().mintable(), + 2, + None, + ); + let sender = context.external.create_account(); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + sender.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + receiver.account_index(), + &token_id, + RawTokenAmount::from(2000), + ); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + utils::execute_token_operations( + &mut context, + &mut block_state, + &token_id, + gov_account.account_index(), + vec![TokenOperation::AddDenyList(TokenListUpdateDetails { + target: CborHolderAccount::from(receiver_addr), + })], + ); + + let sender_addr = context + .external + .account_canonical_address(sender.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(sender_addr), + sender.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, address: Some(address), reason: Some(reason), + }) => { + assert_eq!(address, CborHolderAccount::from(receiver_addr)); + assert_eq!(reason, "recipient in deny list"); + } + ); + assert_eq!( + sender.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); +} + +#[test] +fn test_transfer_paused() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 2, + None, + ); + let receiver = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let gov_account_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let pause_ops = vec![TokenOperation::Pause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&pause_ops)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("pause"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context_with_nonce(gov_account_addr, 2), + gov_account.account_index(), + Payload::TokenUpdate { + payload: TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&vec![TokenOperation::Transfer( + TokenTransfer { + amount: TokenAmount::from_raw(1000, 2), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + }, + )])), + }, + }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(r) => r); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index: 0, address: None, reason: Some(reason), + }) if reason == "token operation transfer is paused" + ); +} diff --git a/plt/plt-scheduler/tests/plt_transactions.rs b/plt/plt-scheduler/tests/plt_transactions.rs new file mode 100644 index 0000000000..d3f2aed1eb --- /dev/null +++ b/plt/plt-scheduler/tests/plt_transactions.rs @@ -0,0 +1,1401 @@ +//! Test of protocol-level token updates. Detailed, functionally complete tests should generally be implemented in +//! the tests of the token module in the `plt-token-module` crate. In the present file, +//! higher level tests are implemented, and they may not in themselves be functionally complete. + +use crate::utils::TokenInitTestParams; +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::base::Energy; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, CborMemo, OperationNotPermittedRejectReason, RawCbor, TokenAmount, TokenId, + TokenListUpdateDetails, TokenListUpdateEventDetails, TokenModuleEventType, + TokenModuleRejectReason, TokenModuleState, TokenOperation, TokenOperationsPayload, + TokenPauseDetails, TokenPauseEventDetails, TokenSupplyUpdateDetails, TokenTransfer, + UnsupportedOperationRejectReason, +}; +use concordium_base::transactions::{Memo, Payload}; +use plt_block_state::entity::entity_test_stub; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::TransactionRejectReason; +use plt_scheduler_types::types::tokens::{RawTokenAmount, TokenHolder}; + +use crate::utils::BlockStateLatest; + +mod utils; + +/// Test protocol-level token transfer. First transfer from governance account. And then perform +/// a second transfer from the destination of the first transfer. +#[test] +fn test_plt_transfer() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + Some(RawTokenAmount::from(5000)), + ); + let account2 = context.external.create_account(); + let account3 = context.external.create_account(); + + // Transfer from governance account to account2 + + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(3000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + + // Assert balance of sender and receiver + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(3000) + ); + + // Assert transfer event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, token_id); + assert_eq!(transfer.amount.amount, RawTokenAmount::from(3000)); + assert_eq!(transfer.amount.decimals, 4); + assert_eq!(transfer.from, TokenHolder::Account(gov_addr)); + assert_eq!(transfer.to, TokenHolder::Account(account2_addr)); + assert_eq!(transfer.memo, None); + }); + + // Transfer from account2 to account3 with memo + + let account3_addr = context + .external + .account_canonical_address(account3.account_index()); + let memo = Memo::try_from(cbor::cbor_encode("testvalue")).unwrap(); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(account3_addr), + memo: Some(CborMemo::Cbor(memo.clone())), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account2_addr), + account2.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + + // Assert balance of sender and receiver + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); + assert_eq!( + account3.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Assert transfer event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, token_id); + assert_eq!(transfer.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(transfer.amount.decimals, 4); + assert_eq!(transfer.from, TokenHolder::Account(account2_addr)); + assert_eq!(transfer.to, TokenHolder::Account(account3_addr)); + assert_eq!(transfer.memo, Some(memo)); + }); +} + +/// Test protocol-level token transfer using address aliases for sender and receiver. +#[test] +fn test_plt_transfer_using_aliases() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + Some(RawTokenAmount::from(5000)), + ); + let account2 = context.external.create_account(); + + let gov_account_address_alias = context + .external + .account_canonical_address(gov_account.account_index()) + .get_alias(5) + .unwrap(); + let account2_alias_address = context + .external + .account_canonical_address(account2.account_index()) + .get_alias(10) + .unwrap(); + + // Transfer from governance account to account2 + + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(3000, 4), + recipient: CborHolderAccount::from(account2_alias_address), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_address_alias), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + // Assert balance of sender and receiver + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(3000) + ); + + // Assert transfer event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, token_id); + assert_eq!(transfer.amount.amount, RawTokenAmount::from(3000)); + assert_eq!(transfer.amount.decimals, 4); + assert_eq!(transfer.from, TokenHolder::Account(gov_account_address_alias)); + assert_eq!(transfer.to, TokenHolder::Account(account2_alias_address)); + assert_eq!(transfer.memo, None); + }); +} + +/// Test protocol-level token transfer that is rejected. +#[test] +fn test_plt_transfer_reject() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + Some(RawTokenAmount::from(5000)), + ); + let account2 = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(10000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply and account balances unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::TokenBalanceInsufficient(_) + ); +} + +/// Test +/// * transfer without sender being in allow list (rejected) +/// * add sender to allow list +/// * transfer (successful) +#[test] +fn test_plt_transfer_allow_list_flow() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().allow_list(), + 4, + Some(RawTokenAmount::from(5000)), + ); + let receiver = context.external.create_account(); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let receiver_addr = context + .external + .account_canonical_address(receiver.account_index()); + + // Add only the sender to the allow list. + let operations = vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.token_id, token_id); + assert_eq!(event.event_type, TokenModuleEventType::AddAllowList.to_type_discriminator()); + let details: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(details.target, CborHolderAccount::from(gov_addr)); + }); + + // Transfer fails because the receiver is not allow-listed yet. + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + address: Some(address), + reason: Some(reason), + .. + }) => { + assert_eq!(address, CborHolderAccount::from(receiver_addr)); + assert_eq!(reason, "recipient not in allow list"); + } + ); + + // Add the receiver to the allow list. + let operations = vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(receiver_addr), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.token_id, token_id); + assert_eq!(event.event_type, TokenModuleEventType::AddAllowList.to_type_discriminator()); + let details: TokenListUpdateEventDetails = cbor::cbor_decode(&event.details).unwrap(); + assert_eq!(details.target, CborHolderAccount::from(receiver_addr)); + }); + + // Transfer succeeds once both accounts are allow-listed. + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(receiver_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + assert_eq!( + receiver.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, token_id); + assert_eq!(transfer.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(transfer.amount.decimals, 4); + assert_eq!(transfer.from, TokenHolder::Account(gov_addr)); + assert_eq!(transfer.to, TokenHolder::Account(receiver_addr)); + assert_eq!(transfer.memo, None); + }); +} + +/// Test add account to allow list for a token where allow lists are not enabled. +#[test] +fn test_plt_allow_list_disabled() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::AddAllowList(TokenListUpdateDetails { + target: CborHolderAccount::from(gov_addr), + })]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::UnsupportedOperation(UnsupportedOperationRejectReason { + operation_type, + reason: Some(reason), + .. + }) => { + assert_eq!(operation_type, "addAllowList"); + assert_eq!(reason, "feature not enabled"); + } + ); +} + +/// Test protocol-level token mint. +#[test] +fn test_plt_mint() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply increased + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(1000) + ); + + // Assert account balance increased + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Assert mint event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenMint(mint) => { + assert_eq!(mint.token_id, token_id); + assert_eq!(mint.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(mint.amount.decimals, 4); + assert_eq!(mint.target, TokenHolder::Account(gov_addr)); + }); +} + +/// Test protocol-level token mint using account address alias. +#[test] +fn test_plt_mint_using_alias() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + + let gov_account_address_alias = context + .external + .account_canonical_address(gov_account.account_index()) + .get_alias(5) + .unwrap(); + + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_address_alias), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply increased + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(1000) + ); + + // Assert account balance increased + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Assert mint event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenMint(mint) => { + assert_eq!(mint.token_id, token_id); + assert_eq!(mint.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(mint.amount.decimals, 4); + assert_eq!(mint.target, TokenHolder::Account(gov_account_address_alias)); + }); +} + +/// Test protocol-level token mint that is rejected. +#[test] +fn test_plt_mint_reject() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + Some(RawTokenAmount::from(5000)), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(u64::MAX, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply and account balance unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!(reject_reason, TokenModuleRejectReason::MintWouldOverflow(_)); +} + +/// Test protocol-level token mint from unauthorized sender. +#[test] +fn test_plt_mint_unauthorized() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (_, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let non_governance_account = context.external.create_account(); + + let non_gov_addr = context + .external + .account_canonical_address(non_governance_account.account_index()); + let operations = vec![TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(non_gov_addr), + non_governance_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply and account balance unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(0) + ); + assert_eq!( + non_governance_account.account_token_balance(&context, token_index), + RawTokenAmount::from(0) + ); + + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(OperationNotPermittedRejectReason { + index, + address, + reason, + }) => { + assert_eq!(index, 0); + assert_eq!( + address, + Some(CborHolderAccount::from(non_gov_addr)) + ); + assert_eq!(reason.as_deref(), Some("sender is not authorized to perform the operation for this token")); + } + ); +} + +/// Test protocol-level token burn. +#[test] +fn test_plt_burn() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 4, + Some(RawTokenAmount::from(5000)), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply decreased + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(4000) + ); + + // Assert account balance decreased + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + + // Assert burn event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenBurn(burn) => { + assert_eq!(burn.token_id, token_id); + assert_eq!(burn.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(burn.amount.decimals, 4); + assert_eq!(burn.target, TokenHolder::Account(gov_addr)); + }); +} + +/// Test protocol-level token burn using address alias for governance account +#[test] +fn test_plt_burn_using_alias() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 4, + Some(RawTokenAmount::from(5000)), + ); + + let gov_account_address_alias = context + .external + .account_canonical_address(gov_account.account_index()) + .get_alias(5) + .unwrap(); + + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(1000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_account_address_alias), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply decreased + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(4000) + ); + + // Assert account balance decreased + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(4000) + ); + + // Assert burn event + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenBurn(burn) => { + assert_eq!(burn.token_id, token_id); + assert_eq!(burn.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(burn.amount.decimals, 4); + assert_eq!(burn.target, TokenHolder::Account(gov_account_address_alias)); + }); +} + +/// Test protocol-level token burn rejection. +#[test] +fn test_plt_burn_reject() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().burnable(), + 4, + Some(RawTokenAmount::from(5000)), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let operations = vec![TokenOperation::Burn(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(10000, 4), + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply and account balance unchanged + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(5000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(5000) + ); + + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::TokenBalanceInsufficient(_) + ); +} + +/// Test multiple protocol-level token update operations in one transaction. +#[test] +fn test_plt_multiple_operations() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, token_index) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let account2 = context.external.create_account(); + + // Compose two operations: Mint and then transfer + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let operations = vec![ + TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(3000, 4), + }), + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + let token = block_state + .token_by_id(&context, &token_id) + .unwrap() + .unwrap(); + + // Assert circulating supply and account balances + assert_eq!( + token.token_p9_base.token_circulating_supply(), + RawTokenAmount::from(3000) + ); + assert_eq!( + gov_account.account_token_balance(&context, token_index), + RawTokenAmount::from(2000) + ); + assert_eq!( + account2.account_token_balance(&context, token_index), + RawTokenAmount::from(1000) + ); + + // Assert two events in right order + assert_eq!(events.len(), 2); + assert_matches!(&events[0], BlockItemEvent::TokenMint(mint) => { + assert_eq!(mint.token_id, token_id); + assert_eq!(mint.amount.amount, RawTokenAmount::from(3000)); + assert_eq!(mint.amount.decimals, 4); + assert_eq!(mint.target, TokenHolder::Account(gov_addr)); + }); + assert_matches!(&events[1], BlockItemEvent::TokenTransfer(transfer) => { + assert_eq!(transfer.token_id, token_id); + assert_eq!(transfer.amount.amount, RawTokenAmount::from(1000)); + assert_eq!(transfer.amount.decimals, 4); + assert_eq!(transfer.from, TokenHolder::Account(gov_addr)); + assert_eq!(transfer.to, TokenHolder::Account(account2_addr)); + assert_eq!(transfer.memo, None); + }); +} + +/// Test protocol-level token "pause" operation. +#[test] +fn test_plt_pause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // Add the "pause" operation + let operations = vec![TokenOperation::Pause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + // Assert that the expected pause event is logged + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.token_id, token_id); + assert_eq!(event.event_type, TokenModuleEventType::Pause.to_type_discriminator()); + let _details: TokenPauseEventDetails = cbor::cbor_decode(&event.details).unwrap(); + }); + + // Assert paused is set in state + let token_info = block_state.query_token_info(&context, &token_id).unwrap(); + let token_module_state: TokenModuleState = + cbor::cbor_decode(&token_info.state.module_state).unwrap(); + assert_eq!(token_module_state.paused, Some(true)); + + // Test transfer is now rejected + let account1 = context.external.create_account(); + let account1_addr = context + .external + .account_canonical_address(account1.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(10000, 4), + recipient: CborHolderAccount::from(account1_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + let reject_reason = utils::assert_token_module_reject_reason(&token_id, reject_reason); + assert_matches!( + reject_reason, + TokenModuleRejectReason::OperationNotPermitted(not_permitted) => { + let reason = not_permitted.reason.unwrap(); + assert!(reason.contains("paused"), "reason: {}", reason); + } + ); +} + +/// Test protocol-level token "unpause" operation. +#[test] +fn test_plt_unpause() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default(), + 4, + None, + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + // Add the "unpause" operation + let operations = vec![TokenOperation::Unpause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let events = assert_matches!(result.outcome, TransactionOutcome::Success(events) => events); + + // Assert that the expected unpause event is logged + assert_eq!(events.len(), 1); + assert_matches!(&events[0], BlockItemEvent::TokenModule(event) => { + assert_eq!(event.token_id, token_id); + assert_eq!(event.event_type, TokenModuleEventType::Unpause.to_type_discriminator()); + let _details: TokenPauseEventDetails = cbor::cbor_decode(&event.details).unwrap(); + }); +} + +/// Test protocol-level token transfer that is rejected because token does not exist. +#[test] +fn test_non_existing_token_id() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let account1 = context.external.create_account(); + let account2 = context.external.create_account(); + + let account1_addr = context + .external + .account_canonical_address(account1.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(1000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let token_id: TokenId = "tokenid1".parse().unwrap(); + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(account1_addr), + account1.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + let reject_reason = assert_matches!(result.outcome, TransactionOutcome::Rejected(reject_reason) => reject_reason); + + assert_matches!( + reject_reason, + TransactionRejectReason::NonExistentTokenId(reject_reason_token_id) => { + assert_eq!(reject_reason_token_id, token_id); + } + ); +} + +/// Test that energy is charged during execution and the correct amount of used energy returned. +#[test] +fn test_energy_charge() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let account2 = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(3000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); + + // Assert energy used + assert_eq!(result.energy_used.energy, 300 + 100); +} + +/// Test that energy is charged during execution and the correct amount of used energy returned, +/// also if the transaction is rejected. +#[test] +fn test_energy_charge_at_reject() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let account2 = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + // Transfer operation with a larger amount than the token balance of the sender `gov_account`, + // which will be the cause of the rejection. + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(10000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + utils::simple_transaction_context(gov_addr), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!( + result.outcome, + TransactionOutcome::Rejected(TransactionRejectReason::TokenUpdateTransactionFailed(_)) + ); + + // Assert energy used + assert_eq!(result.energy_used.energy, 300 + 100); +} + +/// Test that an out of energy reject reason is returned if we run out of energy. +#[test] +fn test_out_of_energy_error() { + let mut context = entity_test_stub::new_stubbed_context(); + let mut block_state = BlockStateLatest::default(); + let token_id: TokenId = "TokenId1".parse().unwrap(); + let (gov_account, _) = utils::create_and_init_token_p11( + &mut context, + &mut block_state, + token_id.clone(), + TokenInitTestParams::default().mintable(), + 4, + None, + ); + let account2 = context.external.create_account(); + utils::increment_account_balance_p11( + &mut context, + &mut block_state, + gov_account.account_index(), + &token_id, + RawTokenAmount::from(5000), + ); + + let gov_addr = context + .external + .account_canonical_address(gov_account.account_index()); + let account2_addr = context + .external + .account_canonical_address(account2.account_index()); + let operations = vec![TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(3000, 4), + recipient: CborHolderAccount::from(account2_addr), + memo: None, + })]; + let payload = TokenOperationsPayload { + token_id: "tokenid1".parse().unwrap(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let result = block_state + .execute_transaction( + &mut context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(150), + sender_account_address: gov_addr, + transaction_sequence_number: 1.into(), + block_timestamp: 0.into(), + }, + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + + // Assert out of energy error + assert_matches!( + result.outcome, + TransactionOutcome::Rejected(TransactionRejectReason::OutOfEnergy) + ); + + // Assert all available energy used + assert_eq!(result.energy_used.energy, 150); +} diff --git a/plt/plt-scheduler/tests/utils/entity_traits/mod.rs b/plt/plt-scheduler/tests/utils/entity_traits/mod.rs new file mode 100644 index 0000000000..eb7d9a5ef1 --- /dev/null +++ b/plt/plt-scheduler/tests/utils/entity_traits/mod.rs @@ -0,0 +1,2 @@ +pub mod scheduler; +mod scheduler_impl; diff --git a/plt/plt-scheduler/tests/utils/entity_traits/scheduler.rs b/plt/plt-scheduler/tests/utils/entity_traits/scheduler.rs new file mode 100644 index 0000000000..4273222509 --- /dev/null +++ b/plt/plt-scheduler/tests/utils/entity_traits/scheduler.rs @@ -0,0 +1,60 @@ +use concordium_base::base::{AccountIndex, Energy, Nonce}; +use concordium_base::contracts_common::{AccountAddress, Timestamp}; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::{RawCbor, TokenId}; +use concordium_base::transactions::Payload; +use concordium_base::updates::UpdatePayload; +use plt_block_state::entity::block_state::{LockNotFoundByIdError, TokenNotFoundByIdError}; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; +use plt_block_state::persistent::blob_reference; +use plt_scheduler::TransactionContext; +use plt_scheduler::scheduler::{ChainUpdateExecutionError, TransactionExecutionError}; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, TransactionExecutionSummary}; +use plt_scheduler_types::types::queries::{TokenAccountInfo, TokenAuthorizations, TokenInfo}; + +/// Operations and queries that the scheduler must support. Must be implemented by all +/// protocol version block states. +pub trait SchedulerOperations { + fn execute_transaction( + &mut self, + context: &mut EntityContext, + transaction_context: TransactionContext, + sender_account: AccountIndex, + payload: Payload, + ) -> Result; + + fn execute_chain_update( + &mut self, + context: &mut EntityContext, + payload: UpdatePayload, + ) -> Result; + + fn query_plt_list(&self, context: &EntityContext) -> Vec; + + fn query_token_info( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result; + + fn query_token_account_infos( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult>; + + fn query_token_authorizations( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result; + + fn query_lock_list(&self, context: &EntityContext) -> Vec; + + fn query_lock_info( + &self, + context: &EntityContext, + lock_id: &LockId, + ) -> Result; +} diff --git a/plt/plt-scheduler/tests/utils/entity_traits/scheduler_impl.rs b/plt/plt-scheduler/tests/utils/entity_traits/scheduler_impl.rs new file mode 100644 index 0000000000..30b592705b --- /dev/null +++ b/plt/plt-scheduler/tests/utils/entity_traits/scheduler_impl.rs @@ -0,0 +1,181 @@ +use super::scheduler::SchedulerOperations; +use concordium_base::base::{AccountIndex, Energy, Nonce}; +use concordium_base::contracts_common::{AccountAddress, Duration, Timestamp}; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::{RawCbor, TokenId}; +use concordium_base::transactions::Payload; +use concordium_base::updates::UpdatePayload; +use hex::ToHex; +use plt_block_state::entity::accounts::Account; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::block_state::{LockNotFoundByIdError, TokenNotFoundByIdError}; +use plt_block_state::entity::{EntityContext, EntityContextTypes}; +use plt_block_state::failure::BlockStateResult; +use plt_block_state::persistent::chain_parameters::p11::PersistentChainParametersP11; +use plt_scheduler::failure::ResultWithBlockStateFailureExt; +use plt_scheduler::scheduler::{ChainUpdateExecutionError, TransactionExecutionError}; +use plt_scheduler::{TransactionContext, protocol_level_locks, scheduler}; +use plt_scheduler::{failure, protocol_level_tokens}; +use plt_scheduler_types::types::execution::{ChainUpdateOutcome, TransactionExecutionSummary}; +use plt_scheduler_types::types::queries::{TokenAccountInfo, TokenAuthorizations, TokenInfo}; +use std::mem; + +impl SchedulerOperations for BlockStateP9 { + fn execute_transaction( + &mut self, + context: &mut EntityContext, + transaction_context: TransactionContext, + sender_account: AccountIndex, + payload: Payload, + ) -> Result { + let sender_account = Account::from_existing_account(sender_account); + + scheduler::p9::execute_transaction( + context, + self, + transaction_context, + sender_account.clone(), + payload, + ) + } + + fn execute_chain_update( + &mut self, + context: &mut EntityContext, + payload: UpdatePayload, + ) -> Result { + scheduler::p9::execute_chain_update(context, self, payload) + } + + fn query_plt_list(&self, context: &EntityContext) -> Vec { + protocol_level_tokens::p9::query_plt_list(context, self).unwrap() + } + + fn query_token_info( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result { + protocol_level_tokens::p9::query_token_info(context, self, token_id) + .nest() + .unwrap() + } + + fn query_token_account_infos( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult> { + protocol_level_tokens::p9::query_token_account_infos( + context, + self, + Account::from_existing_account(account), + ) + } + + fn query_token_authorizations( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result { + protocol_level_tokens::p9::query_token_authorizations(context, self, token_id) + .nest() + .unwrap() + } + + fn query_lock_list(&self, context: &EntityContext) -> Vec { + protocol_level_locks::p9::query_lock_list(context, self).unwrap() + } + + fn query_lock_info( + &self, + context: &EntityContext, + lock_id: &LockId, + ) -> Result { + protocol_level_locks::p9::query_lock_info(context, self, lock_id) + .nest() + .unwrap() + } +} + +impl SchedulerOperations for BlockStateP11 { + fn execute_transaction( + &mut self, + context: &mut EntityContext, + transaction_context: TransactionContext, + sender_account: AccountIndex, + payload: Payload, + ) -> Result { + let sender_account = Account::from_existing_account(sender_account); + + scheduler::p11::execute_transaction( + context, + self, + transaction_context, + sender_account.clone(), + payload, + &PersistentChainParametersP11 { + max_lock_duration: Duration::from_millis(u64::MAX), + }, + ) + } + + fn execute_chain_update( + &mut self, + context: &mut EntityContext, + payload: UpdatePayload, + ) -> Result { + scheduler::p11::execute_chain_update(context, self, payload) + } + + fn query_plt_list(&self, context: &EntityContext) -> Vec { + protocol_level_tokens::p11::query_plt_list(context, self).unwrap() + } + + fn query_token_info( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result { + protocol_level_tokens::p11::query_token_info(context, self, token_id) + .nest() + .unwrap() + } + + fn query_token_account_infos( + &self, + context: &EntityContext, + account: AccountIndex, + ) -> BlockStateResult> { + protocol_level_tokens::p11::query_token_account_infos( + context, + self, + Account::from_existing_account(account), + ) + } + + fn query_token_authorizations( + &self, + context: &EntityContext, + token_id: &TokenId, + ) -> Result { + protocol_level_tokens::p11::query_token_authorizations(context, self, token_id) + .nest() + .unwrap() + } + + fn query_lock_list(&self, context: &EntityContext) -> Vec { + protocol_level_locks::p11::query_lock_list(context, self).unwrap() + } + + fn query_lock_info( + &self, + context: &EntityContext, + lock_id: &LockId, + ) -> Result { + protocol_level_locks::p11::query_lock_info(context, self, lock_id) + .nest() + .unwrap() + } +} diff --git a/plt/plt-scheduler/tests/utils/lock.rs b/plt/plt-scheduler/tests/utils/lock.rs new file mode 100644 index 0000000000..b97cef787d --- /dev/null +++ b/plt/plt-scheduler/tests/utils/lock.rs @@ -0,0 +1,132 @@ +use crate::utils::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::common::cbor; +use concordium_base::common::types::TransactionTime; +use concordium_base::protocol_level_locks::LockId; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaUpdateOperation, MetaUpdateOperations, MetaUpdatePayload, +}; +use concordium_base::protocol_level_tokens::{CborHolderAccount, RawCbor, TokenId}; +use concordium_base::transactions::Payload; +use plt_block_state::entity::EntityContext; +use plt_block_state::entity::accounts::Accounts; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::entity_test_stub::StubbedEntityContext; +use plt_block_state::persistent::protocol_level_locks::p11::LockControllerSimpleV0Grant; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +/// Simple configuration for creating a lock in tests. +#[derive(Debug, Clone)] +pub struct CreateLockSimpleConfig { + pub recipients: Vec, + pub grants: Vec, + pub tokens: Vec, + pub expiry: u64, + pub keep_alive: bool, +} + +/// Create a lock in the block state. +pub fn create_lock( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateP11, + lock_id: &LockId, + config: CreateLockSimpleConfig, +) { + use concordium_base::protocol_level_locks::*; + use concordium_base::protocol_level_tokens::meta_operations::*; + let sender = context + .account_by_index(lock_id.account_index()) + .expect("sender account must exist"); + let resolve_account = |index: &AccountIndex| { + CborHolderAccount::from( + context + .account_by_index(*index) + .unwrap_or_else(|_| panic!("account index {} does not exist", *index)) + .canonical_account_address, + ) + }; + let recipients = + LockRecipients::Limited(config.recipients.iter().map(resolve_account).collect()); + let grants = config + .grants + .iter() + .map(|grant| LockControllerSimpleV0Grant { + account: resolve_account(&grant.account), + roles: grant.roles.clone(), + }) + .collect(); + let operations = MetaUpdateOperations { + operations: vec![lock_create(LockConfig { + recipients, + expiry: TransactionTime::from(config.expiry), + controller: LockController::SimpleV0(LockControllerSimpleV0 { + grants, + tokens: config.tokens, + keep_alive: config.keep_alive, + memo: None, + }), + metadata: None, + })], + }; + + block_state + .execute_transaction( + context, + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address: sender.canonical_account_address, + transaction_sequence_number: lock_id.sequence_number(), + block_timestamp: 0.into(), + }, + sender.account.account_index(), + Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }, + }, + ) + .expect("create lock transaction must succeed"); +} + +/// Track a `(account, token)` balance reference under the given lock and record the +/// locked `amount` for the account in the token-module key-value state. +/// +/// TODO: (COR-2305) Once lock-operation transaction payloads land (the ones that +/// move balances into / out of locks), this helper should drive those payloads +/// through `scheduler::execute_transaction` (mirroring `increment_account_balance`) +/// instead of poking the block state and key-value store directly. At that point +/// the constants duplicated above can also be removed. +pub fn lock_balance( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateP11, + lock_id: &LockId, + funder_account: AccountIndex, + token_id: &TokenId, + amount: RawTokenAmount, +) { + // Get the token and determine its index + let token = block_state + .token_by_id(context, token_id) + .unwrap() + .expect("token must exist"); + let token_index = token.token_p9_base.token_index(); + + // Register the (account, token) pair in the lock state + let mut lock = block_state + .lock_by_id(context, lock_id) + .unwrap() + .expect("lock must exist"); + lock.add_lock_balance_ref(funder_account, token_index); + block_state.update_lock(context, lock).unwrap(); + + // Set the locked amount in the token module KV state + let mut token = block_state + .token_by_id(context, token_id) + .unwrap() + .expect("token must exist"); + token + .set_locked_balance_for_account(context, funder_account, lock_id, amount) + .unwrap(); + block_state.update_token(context, token).unwrap(); +} diff --git a/plt/plt-scheduler/tests/utils/mod.rs b/plt/plt-scheduler/tests/utils/mod.rs new file mode 100644 index 0000000000..d19f452414 --- /dev/null +++ b/plt/plt-scheduler/tests/utils/mod.rs @@ -0,0 +1,44 @@ +// Allow items in this file to be unused. This is needed because it is imported from multiple +// compile targets (each of the integration tests), and some of the targets may not use all +// items in the file. +#![allow(unused)] + +pub mod entity_traits; +mod lock; +mod token; + +use concordium_base::{base::Energy, contracts_common::AccountAddress, transactions}; +pub use lock::*; +pub use token::*; + +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_scheduler_types::types::protocol_version::ProtocolVersion; + +/// The latest protocol version supported by the scheduler, used as default in tests. +pub const LATEST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::P11; + +pub type BlockStateLatest = BlockStateP11; + +/// Creates a [`TransactionContext`] with the given sender account address and +/// transaction sequence number. The energy limit is set to the maximum value, +/// and the block timestamp is set to 0. +pub fn simple_transaction_context_with_nonce( + sender_account_address: AccountAddress, + transaction_sequence_number: u64, +) -> plt_scheduler::TransactionContext { + plt_scheduler::TransactionContext { + energy_limit: Energy::from(u64::MAX), + sender_account_address, + transaction_sequence_number: transaction_sequence_number.into(), + block_timestamp: 0.into(), + } +} + +/// Creates a [`TransactionContext`] with the given sender account address. +/// The energy limit is set to the maximum value, and the block timestamp is set +/// to 0. The transaction sequence number is set to 1. +pub fn simple_transaction_context( + sender_account_address: AccountAddress, +) -> plt_scheduler::TransactionContext { + simple_transaction_context_with_nonce(sender_account_address, 1) +} diff --git a/plt/plt-scheduler/tests/utils/token.rs b/plt/plt-scheduler/tests/utils/token.rs new file mode 100644 index 0000000000..a8156e403c --- /dev/null +++ b/plt/plt-scheduler/tests/utils/token.rs @@ -0,0 +1,345 @@ +use crate::utils; + +use super::entity_traits::scheduler::SchedulerOperations; +use assert_matches::assert_matches; +use concordium_base::base::{AccountIndex, Energy}; +use concordium_base::common::cbor; +use concordium_base::protocol_level_tokens::meta_operations::{ + MetaUpdateOperation, MetaUpdateOperations, MetaUpdatePayload, +}; +use concordium_base::protocol_level_tokens::{ + CborHolderAccount, MetadataUrl, RawCbor, TokenAmount, TokenId, + TokenModuleInitializationParameters, TokenModuleRejectReason, TokenModuleRejectReasonType, + TokenModuleState, TokenOperation, TokenOperationsPayload, TokenPauseDetails, + TokenSupplyUpdateDetails, TokenTransfer, +}; +use concordium_base::transactions::Payload; +use concordium_base::updates::{CreatePlt, UpdatePayload}; +use plt_block_state::entity::EntityContext; +use plt_block_state::entity::accounts::{Account, Accounts}; +use plt_block_state::entity::block_state::p9::BlockStateP9; +use plt_block_state::entity::block_state::p11::BlockStateP11; +use plt_block_state::entity::entity_test_stub::StubbedEntityContext; +use plt_block_state::persistent::protocol_level_tokens::p9::TokenIndex; +use plt_scheduler::TOKEN_MODULE_REF; +use plt_scheduler_types::types::events::BlockItemEvent; +use plt_scheduler_types::types::execution::TransactionOutcome; +use plt_scheduler_types::types::reject_reasons::{ + EncodedTokenModuleRejectReason, TransactionRejectReason, +}; +use plt_scheduler_types::types::tokens::RawTokenAmount; + +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub struct TokenInitTestParams { + allow_list: Option, + deny_list: Option, + mintable: Option, + burnable: Option, +} + +impl TokenInitTestParams { + pub fn allow_list(self) -> Self { + Self { + allow_list: Some(true), + ..self + } + } + + pub fn deny_list(self) -> Self { + Self { + deny_list: Some(true), + ..self + } + } + + pub fn mintable(self) -> Self { + Self { + mintable: Some(true), + ..self + } + } + + pub fn burnable(self) -> Self { + Self { + burnable: Some(true), + ..self + } + } +} + +/// Create and initialize token in the stub. Returns the governance account for the token. +pub fn create_and_init_token_p9( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateP9, + token_id: TokenId, + params: TokenInitTestParams, + decimals: u8, + initial_supply: Option, +) -> (Account, TokenIndex) { + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: params.allow_list, + deny_list: params.deny_list, + initial_supply: initial_supply.map(|raw| TokenAmount::from_raw(raw.into(), decimals)), + mintable: params.mintable, + burnable: params.burnable, + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals, + initialization_parameters, + }); + block_state + .execute_chain_update(context, payload) + .expect("create and initialize token"); + + let token_index = block_state + .token_by_id(context, &token_id) + .unwrap() + .unwrap() + .token_p9_base + .token_index(); + + (gov_account, token_index) +} + +/// Create and initialize token in the stub. Returns the governance account for the token. +pub fn create_and_init_token_p11( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateP11, + token_id: TokenId, + params: TokenInitTestParams, + decimals: u8, + initial_supply: Option, +) -> (Account, TokenIndex) { + let gov_account = context.external.create_account(); + let gov_holder_account = CborHolderAccount::from( + context + .external + .account_canonical_address(gov_account.account_index()), + ); + let metadata = MetadataUrl::from("https://plt.token".to_string()); + let parameters = TokenModuleInitializationParameters { + name: Some("Protocol-level token".to_owned()), + metadata: Some(metadata.clone()), + governance_account: Some(gov_holder_account.clone()), + allow_list: params.allow_list, + deny_list: params.deny_list, + initial_supply: initial_supply.map(|raw| TokenAmount::from_raw(raw.into(), decimals)), + mintable: params.mintable, + burnable: params.burnable, + }; + let initialization_parameters = cbor::cbor_encode(¶meters).into(); + + let payload = UpdatePayload::CreatePlt(CreatePlt { + token_id: token_id.clone(), + token_module: TOKEN_MODULE_REF, + decimals, + initialization_parameters, + }); + block_state + .execute_chain_update(context, payload) + .expect("create and initialize token"); + + let token_index = block_state + .token_by_id(context, &token_id) + .unwrap() + .unwrap() + .token_p9_base + .token_index(); + + (gov_account, token_index) +} + +/// Add amount to account balance in the stub. This is done by minting +/// and transferring the given amount +pub fn increment_account_balance_p11( + context: &mut StubbedEntityContext, + block_state: &mut BlockStateP11, + account_index: AccountIndex, + token_id: &TokenId, + balance: RawTokenAmount, +) { + let token = block_state + .token_by_id(context, token_id) + .unwrap() + .expect("created token"); + let token_configuration = token.token_p9_base.token_configuration(context).unwrap(); + let operations = vec![ + TokenOperation::Mint(TokenSupplyUpdateDetails { + amount: TokenAmount::from_raw(balance.into(), token_configuration.decimals), + }), + TokenOperation::Transfer(TokenTransfer { + amount: TokenAmount::from_raw(balance.into(), token_configuration.decimals), + recipient: CborHolderAccount::from( + context.external.account_canonical_address(account_index), + ), + memo: None, + }), + ]; + let payload = TokenOperationsPayload { + token_id: token_configuration.token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + + let token_info = block_state + .query_token_info(context, &token_configuration.token_id) + .unwrap(); + let token_module_state: TokenModuleState = + cbor::cbor_decode(&token_info.state.module_state).unwrap(); + let gov_account = EntityContext::account_by_address( + context, + &token_module_state + .governance_account + .as_ref() + .unwrap() + .address, + ) + .unwrap(); + + let outcome = block_state + .execute_transaction( + context, + utils::simple_transaction_context( + token_module_state.governance_account.unwrap().address, + ), + gov_account.account_index(), + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(outcome.outcome, TransactionOutcome::Success(_)); +} + +/// Pause the given token as the governance account. Panics if the operation fails. +pub fn pause_token( + context: &mut StubbedEntityContext, + block_state: &mut impl SchedulerOperations, + token_id: &TokenId, + gov_account: AccountIndex, +) { + let operations = vec![TokenOperation::Pause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_addr = context.external.account_canonical_address(gov_account); + let result = block_state + .execute_transaction( + context, + utils::simple_transaction_context(gov_addr), + gov_account, + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); +} + +/// Unpause the given token as the governance account. Panics if the operation fails. +pub fn unpause_token( + context: &mut StubbedEntityContext, + block_state: &mut impl SchedulerOperations, + token_id: &TokenId, + gov_account: AccountIndex, +) { + let operations = vec![TokenOperation::Unpause(TokenPauseDetails {})]; + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let gov_addr = context.external.account_canonical_address(gov_account); + let result = block_state + .execute_transaction( + context, + utils::simple_transaction_context(gov_addr), + gov_account, + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(_)); +} + +/// Execute token operations as the given sender account. Returns the block item events on +/// success, panics if the transaction fails. +pub fn execute_token_operations( + context: &mut StubbedEntityContext, + block_state: &mut impl SchedulerOperations, + token_id: &TokenId, + sender: AccountIndex, + operations: Vec, +) -> Vec { + let payload = TokenOperationsPayload { + token_id: token_id.clone(), + operations: RawCbor::from(cbor::cbor_encode(&operations)), + }; + let sender_addr = context.external.account_canonical_address(sender); + let result = block_state + .execute_transaction( + context, + utils::simple_transaction_context(sender_addr), + sender, + Payload::TokenUpdate { payload }, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, TransactionOutcome::Success(events) => events) +} + +/// Execute meta-update operations as the given sender account. Returns the block item events on +/// success, panics if the transaction fails. +pub fn execute_meta_operations( + context: &mut StubbedEntityContext, + block_state: &mut impl SchedulerOperations, + sender: AccountIndex, + operations: Vec, +) -> Vec { + let payload = Payload::MetaUpdate { + payload: MetaUpdatePayload { + operations: RawCbor::from(cbor::cbor_encode(&MetaUpdateOperations { operations })), + }, + }; + let sender_addr = context.external.account_canonical_address(sender); + let result = block_state + .execute_transaction( + context, + crate::utils::simple_transaction_context(sender_addr), + sender, + payload, + ) + .expect("transaction internal error"); + assert_matches!(result.outcome, plt_scheduler_types::types::execution::TransactionOutcome::Success(events) => events) +} + +fn decode_token_module_reject_reason( + reject_reason: &EncodedTokenModuleRejectReason, +) -> TokenModuleRejectReason { + let reject_reason_type = + TokenModuleRejectReasonType::try_from_type_discriminator(&reject_reason.reason_type) + .unwrap(); + TokenModuleRejectReason::decode_reject_reason( + reject_reason_type, + reject_reason.details.as_ref().unwrap(), + ) + .unwrap() +} + +pub fn assert_token_module_reject_reason( + token_id: &TokenId, + reject_reason: TransactionRejectReason, +) -> TokenModuleRejectReason { + let reject_reason = assert_matches!( + &reject_reason, + TransactionRejectReason::TokenUpdateTransactionFailed(reject_reason) => reject_reason); + assert_eq!(reject_reason.token_id, *token_id); + decode_token_module_reject_reason(reject_reason) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..cb97dd5112 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +# The toolchain version here must be in sync (down to patch version) with the toolchain version +# used in the static_libraries image found in the workflow of `.github/workflow/release.yaml`, +# otherwise it causes issues during linking the static builds. +[toolchain] +channel = "1.95.0" diff --git a/scripts/distribution/windows/build-all.ps1 b/scripts/distribution/windows/build-all.ps1 index 9d87bc4aff..1d812c62fc 100644 --- a/scripts/distribution/windows/build-all.ps1 +++ b/scripts/distribution/windows/build-all.ps1 @@ -1,18 +1,30 @@ -param ([string] $rustVersion = "1.73", [string] $nodeVersion) +param ([string] $rustVersion, [string] $nodeVersion) Write-Output "stack version: $(stack --version)" Write-Output "cargo version: $(cargo --version)" Write-Output "flatc version: $(flatc --version)" Write-Output "protoc version: $(protoc --version)" -# Set the default rust toolchain so that consensus rust dependencies use it. -rustup default $rustVersion-x86_64-pc-windows-gnu +# Override the rust toolchain so that consensus rust dependencies use it. +rustup override set $rustVersion-x86_64-pc-windows-gnu + +# The reason we "prebuild" the node Rust library dependency: +# When Setup.hs is run (which invokes the cargo build), stack puts +# the LLVM version of dlltool on the path, which is not compatible with the gnu dlltool. +# It thus does not generate the expected import lib. By the rust upfront, when it is +# subsequently built in Setup.hs it will already be built and so does not get rebuilt. +Write-Output "Prebuilding node Rust library..." +Push-Location plt +rustup show active-toolchain +cargo build --release --locked -p node-rust-library +Pop-Location Write-Output "Building consensus..." stack build if ($LASTEXITCODE -ne 0) { throw "Failed building consensus" } Write-Output "Building node..." +stack exec -- rustup show active-toolchain stack exec -- cargo build --manifest-path concordium-node\Cargo.toml --release --locked if ($LASTEXITCODE -ne 0) { throw "Failed building node" } @@ -59,8 +71,8 @@ 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\node_rust_library.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/scripts/static-binaries/build-on-ubuntu.sh b/scripts/static-binaries/build-on-ubuntu.sh index 149df51656..4c2fcf2feb 100755 --- a/scripts/static-binaries/build-on-ubuntu.sh +++ b/scripts/static-binaries/build-on-ubuntu.sh @@ -13,7 +13,7 @@ set -euxo pipefail extra_features=${EXTRA_FEATURES:-""} -REQUIRED_PARAMETERS=("PROTOC_VERSION" "FLATBUFFERS_VERSION" "RUST_TOOLCHAIN_VERSION") +REQUIRED_PARAMETERS=("PROTOC_VERSION" "FLATBUFFERS_VERSION") # Loop through the required variables and check if they are set for VAR in "${REQUIRED_PARAMETERS[@]}"; do @@ -50,7 +50,6 @@ rm protoc.zip curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" rustup set profile minimal -rustup default "$RUST_TOOLCHAIN_VERSION" ASSETS_URL="https://github.com/google/flatbuffers/releases/expanded_assets/v${FLATBUFFERS_VERSION}" GCC_VERSION=$(curl -s "$ASSETS_URL" | grep -oP 'Linux\.flatc\.binary\.g\+\+\-\K[0-9]+' | head -n 1) diff --git a/scripts/static-binaries/static-binaries.Dockerfile b/scripts/static-binaries/static-binaries.Dockerfile index b1993260c7..453cd721b8 100644 --- a/scripts/static-binaries/static-binaries.Dockerfile +++ b/scripts/static-binaries/static-binaries.Dockerfile @@ -29,14 +29,12 @@ ARG build_version ARG extra_features ARG protoc_version ARG flatbuffers_version -ARG rust_toolchain_version WORKDIR /build RUN BRANCH="${branch}" \ EXTRA_FEATURES="${extra_features}" \ PROTOC_VERSION="${protoc_version}" \ FLATBUFFERS_VERSION="${flatbuffers_version}" \ - RUST_TOOLCHAIN_VERSION="${rust_toolchain_version}" \ /build-on-ubuntu.sh # The binaries are available in the diff --git a/scripts/static-libraries/build-static-libraries.sh b/scripts/static-libraries/build-static-libraries.sh index d58c271221..f40c87db10 100755 --- a/scripts/static-libraries/build-static-libraries.sh +++ b/scripts/static-libraries/build-static-libraries.sh @@ -8,7 +8,7 @@ set -ex mkdir -p /target/{profiling,vanilla}/{ghc,dependencies,concordium} mkdir -p /binaries/{lib,bin} -LIB_DIR=$(stack --stack-yaml /build/concordium-consensus/stack.static.yaml ghc -- --print-libdir) +LIB_DIR=$(stack --stack-yaml /build/stack.static.yaml ghc -- --print-libdir) find "$LIB_DIR" -type f -name "*_p.a" ! -name "*_debug_p.a" ! -name "*rts_p.a" ! -name "*ffi_p.a" -exec cp {} /target/profiling/ghc/ \; find "$LIB_DIR" -type f -name "*.a" ! -name "*_p.a" ! -name "*_l.a" ! -name "*_debug.a" ! -name "*rts.a" ! -name "*ffi.a" -exec cp {} /target/vanilla/ghc/ \; @@ -16,26 +16,28 @@ find "$LIB_DIR" -type f -name "*.a" ! -name "*_p.a" ! -name "*_l.a" ! -name "*_d cd /build ############################################################################################################################# -## Build the project +## Build the project and copy Haskell libraries -stack build --profile --flag "concordium-consensus:-dynamic" --stack-yaml /build/concordium-consensus/stack.static.yaml -find /build/concordium-consensus/.stack-work -type f -name "*.a" ! -name "*_p.a" -exec cp {} /target/vanilla/concordium/ \; -find /build/concordium-consensus/.stack-work -type f -name "*_p.a" -exec cp {} /target/profiling/concordium/ \; +stack build --profile --flag "concordium-consensus:-dynamic" --stack-yaml /build/stack.static.yaml +find /build/.stack-work -type f -name "*.a" ! -name "*_p.a" -exec cp {} /target/vanilla/concordium/ \; +find /build/.stack-work -type f -name "*_p.a" -exec cp {} /target/profiling/concordium/ \; ############################################################################################################################# -## Copy rust binaries +## Copy Haskell binaries and their Rust dependencies -LOCAL_INSTALL_ROOT=$(stack --stack-yaml /build/concordium-consensus/stack.static.yaml path --profile --local-install-root) +LOCAL_INSTALL_ROOT=$(stack --stack-yaml /build/stack.static.yaml path --profile --local-install-root) cp "$LOCAL_INSTALL_ROOT"/bin/{generate-update-keys,genesis,database-exporter} /binaries/bin/ cp /build/concordium-base/rust-src/target/release/*.so /binaries/lib/ -cp /build/concordium-base/smart-contracts/wasm-chain-integration/target/release/*.so /binaries/lib/ +cp /build/concordium-consensus/lib/*.so /binaries/lib/ ############################################################################################################################# -## Copy dependencies +## Copy Haskell dependencies find ~/.stack/snapshots/x86_64-linux/ -type f -name "*.a" ! -name "*_p.a" -exec cp {} /target/vanilla/dependencies \; find ~/.stack/snapshots/x86_64-linux/ -type f -name "*_p.a" -exec cp {} /target/profiling/dependencies \; +## Rust dependencies - copy the static libraries to the target directory, and strip debug symbols from them to reduce their size, as +## they are not needed for the final binaries and can cause linking issues if they contain ruststd symbols mkdir -p /target/rust cp -r /build/concordium-base/rust-src/target/release/*.a /target/rust/ @@ -47,6 +49,7 @@ find /target /binaries -type f -exec strip --strip-debug {} \; ( cd /target/rust + ## takes every .a static file and unpacks them for i in $(ls) do ar x $i; @@ -55,6 +58,7 @@ find /target /binaries -type f -exec strip --strip-debug {} \; set +e + ## delete rust specific runtime symbols from the .o files, as they are not needed and cause linking issues for file in $(find . -type f -name "*.o"); do if nm $file | grep "\(T __rust_alloc\)\|\(T __rdl_alloc\)\|\(T __clzsi2\)\|\(T rust_eh_personality\)" >> /dev/null; then echo "Removing file:"; @@ -65,12 +69,14 @@ find /target /binaries -type f -exec strip --strip-debug {} \; set -e + ## creates the librcrypto.a static library from the remaining .o files, which should now be free of ruststd symbols ar rcs libRcrypto.a *.o rm *.o - cp /build/concordium-base/smart-contracts/wasm-chain-integration/target/release/libconcordium_smart_contract_engine.a /target/rust/libconcordium_smart_contract_engine.a + ## copy Rust node library and repeat the process to remove ruststd symbols from it as well, as it is linked into the consensus node and must not contain ruststd symbols + cp /build/concordium-consensus/lib/libnode_rust_library.a /target/rust/libnode_rust_library.a - ar x libconcordium_smart_contract_engine.a + ar x libnode_rust_library.a set +e @@ -85,9 +91,8 @@ find /target /binaries -type f -exec strip --strip-debug {} \; set -e - - rm libconcordium_smart_contract_engine.a - ar rcs libconcordium_smart_contract_engine.a *.o + rm libnode_rust_library.a + ar rcs libnode_rust_library.a *.o rm *.o ) diff --git a/service/windows/installer/Node.wxs b/service/windows/installer/Node.wxs index d65fcc3854..1cce879cda 100644 --- a/service/windows/installer/Node.wxs +++ b/service/windows/installer/Node.wxs @@ -26,7 +26,7 @@ - + diff --git a/service/windows/installer/build.ps1 b/service/windows/installer/build.ps1 index 3025ceac31..4b3eb702f2 100644 --- a/service/windows/installer/build.ps1 +++ b/service/windows/installer/build.ps1 @@ -21,7 +21,7 @@ try { "-b", "consensus=$StackInstallRoot\lib", "-b", "node=..\..\..\concordium-node\target\release", "-b", "baselib=..\..\..\concordium-base\lib", - "-b", "contractlib=..\..\..\concordium-base\smart-contracts\lib", + "-b", "rustlib=..\..\..\concordium-consensus\lib", "-b", "collector=..\..\..\collector\target\release", "-b", "service=..\target\x86_64-pc-windows-msvc\release" "-b", "ca=.\custom-actions\target\x86_64-pc-windows-msvc\release", diff --git a/service/windows/src/main.rs b/service/windows/src/main.rs index 97d248153a..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/concordium-consensus/stack.static.yaml b/stack.static.yaml similarity index 66% rename from concordium-consensus/stack.static.yaml rename to stack.static.yaml index d9bb8acafe..0e4a1f7246 100644 --- a/concordium-consensus/stack.static.yaml +++ b/stack.static.yaml @@ -1,15 +1,15 @@ resolver: lts-24.0 packages: -- . -- haskell-lmdb -- ../concordium-base +- ./concordium-base +- ./concordium-consensus +- ./concordium-consensus/haskell-lmdb extra-deps: extra-lib-dirs: -- ../concordium-base/lib -- ../concordium-base/smart-contracts/lib +- ./concordium-base/lib +- ./concordium-consensus/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/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