diff --git a/.gitignore b/.gitignore index 6046b93b12..b26fe2b766 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ qa/.metals/metals.lock.db qa/.metals/metals.mv.db qa/.metals/metals.log .scalafmt.conf + +# Jacoco reports +coverage-reports/** diff --git a/.travis.yml b/.travis.yml index 5688ccffc4..de42b54516 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,9 @@ env: global: # Separate branches by space - PROD_RELEASE_BRANCHES='master' + - NODE_VERSION="v16" + - TEST_CMD="./run_sc_tests.sh" + - API_ZEN_REPO_URL="https://api.github.com/repos/HorizenOfficial/zen" before_script: - source ci/setup_env.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index f53c43d0be..eb584c3390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +**0.9.0** +1. libevm dependency updated to 1.0.0. +2. Added support for EVM and native smart contracts interoperability. +3. Sparkz dependency updated to 2.2.0. +4. Improved storage versioning (fullsynch time reduced by 5x) +5. Minor fixes: + * [eth RPC endpoint] debug_traceCall now returns a more accurate error response for reverted transactions + * [eth RPC endpoint] debug_traceCall and debug_traceTransaction now return a correct value for the gasUsed field when topmost call is a call to a Solidity Smart Contract function. + * Certificates older than 4 epochs are now deleted from the storage only if more recent certificates appeared. + **0.8.1** 1. Improved precision of eth_gasPrice RPC call @@ -141,4 +151,4 @@ 6. Possibility to declare custom Transactions/Boxes/Secrets/etc. 7. Possibility to extend/manage basic API. 8. Web interface and command line tool for interaction with the Node. -9. Sidechain Bootstrapping Tool to configure sidechain network according to the mainchain network. \ No newline at end of file +9. Sidechain Bootstrapping Tool to configure sidechain network according to the mainchain network. diff --git a/README.md b/README.md index 5d123c36cd..77c1f0e362 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ For more details see [zendoo-sc-cryptolib](https://github.com/HorizenOfficial/ze * Java 11 or newer * Scala 2.12.10+ -* Python 3 +* Python 3.10 * Maven On some Linux OSs during backward transfers certificates proofs generation a extremely big RAM consumption may happen, that will lead to the process force killing by the OS. @@ -48,7 +48,7 @@ While we keep monitoring the memory footprint of the proofs generation process, - After the installation, just run `export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1` before starting the sidechain node, or run the sidechain node adding `LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1` at the beginning of the java command line as follows: ``` -LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 java -cp ./target/sidechains-sdk-simpleapp-0.8.1.jar:./target/lib/* io.horizen.examples.SimpleApp +LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 java -cp ./target/sidechains-sdk-simpleapp-0.9.0.jar:./target/lib/* io.horizen.examples.SimpleApp ``` - In the folder `ci` you will find the script `run_sc.sh` to automatically check and use jemalloc library while starting the sidechain node. diff --git a/ci/run_python_tests.sh b/ci/run_python_tests.sh new file mode 100644 index 0000000000..c3e1172814 --- /dev/null +++ b/ci/run_python_tests.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +set -euo pipefail + +# Functions +function fn_die() { + echo -e "$1" >&2 + exit "${2:-1}" +} + +function import_gpg_keys() { + # shellcheck disable=SC2207 + declare -r my_arr=( $(echo "${@}" | tr " " "\n") ) + + if [ "${#my_arr[@]}" -eq 0 ]; then + fn_die "Error: There are ZERO gpg keys to import. ZEN_REPO_MAINTAINER_KEYS variable is not set. Exiting ..." + else + # shellcheck disable=SC2145 + printf "%s\n" "Tagged build, fetching keys:" "${@}" "" + for key in "${my_arr[@]}"; do + gpg -v --batch --keyserver hkps://keys.openpgp.org --recv-keys "${key}" || + gpg -v --batch --keyserver hkp://keyserver.ubuntu.com --recv-keys "${key}" || + gpg -v --batch --keyserver hkp://pgp.mit.edu:80 --recv-keys "${key}" || + gpg -v --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys "${key}" || + { fn_die "Error: ${key} can not be found on GPG key servers. Please upload it to at least one of the following GPG key servers:\nhttps://keys.openpgp.org/\nhttps://keyserver.ubuntu.com/\nhttps://pgp.mit.edu/ Exiting ...";} + done + fi +} + +function check_signed_tag() { + local tag="${1}" + + if git verify-tag -v "${tag}"; then + echo "${tag} is a valid signed tag" + else + fn_die "Error: ${tag} signature is NOT valid. Exiting ..." + fi +} + + +if [ -z "${NODE_VERSION:-}" ]; then + fn_die "Error: NODE_VERSION variable is not set. Exiting ..." +fi + +if [ -z "${TEST_CMD:-}" ]; then + fn_die "Error: TEST_CMD variable is not set. Exiting ..." +fi + +if [ -z "${TEST_ARGS:-}" ]; then + fn_die "Error: TEST_ARGS variable is not set. Exiting ..." +fi + +if [ -z "${API_ZEN_REPO_URL:-}" ]; then + fn_die "Error: API_ZEN_REPO_URL variable is not set. Exiting ..." +fi + +if [ -z "${ZEN_REPO_MAINTAINER_KEYS:-}" ]; then + fn_die "Error: ZEN_REPO_MAINTAINER_KEYS variable is not set. Exiting ..." +fi + +CURRENT_DIR="${PWD}" + +# Step 1 +echo "" && echo "=== Get latest ZEN repo PROD build id and commit hash ===" && echo "" + +zen_tag="$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/HorizenOfficial/zen/git/refs/tags | jq -r '[.[] | select(.ref | test("refs/tags/v[0-9]\\.[0-9]\\.[0-9]$"))][-1].ref' | sed -e 's|refs/tags/||')" +check_runs="$(curl -sL -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${API_ZEN_REPO_URL}/commits/${zen_tag}/check-runs")" +travis_build_id="$(basename "$(jq -rc '.check_runs[0].details_url' <<< "${check_runs}")")" +commit_sha="$(jq -rc '.check_runs[0].head_sha' <<< "${check_runs}")" + +travis_urls=" +#amd64 +https://f001.backblazeb2.com/file/ci-horizen/amd64-linux-ubuntu_focal-${travis_build_id}-${commit_sha}.tar.gz.sha256 +https://f001.backblazeb2.com/file/ci-horizen/amd64-linux-ubuntu_focal-${travis_build_id}-${commit_sha}.tar.gz +" + + +# Step 2 +echo "" && echo "=== Create folder structure ===" && echo "" + +base_dir="${CURRENT_DIR}/zen_release" +is_cached=false + +if [ -d "${base_dir}/travis_files" ]; then + echo "${base_dir} folder already exists, using cache!" + cd "${base_dir}"/travis_files + is_cached=true +else + mkdir -p "${base_dir}"/{travis_files,src} + + # Step 3 + echo "" && echo "=== Downloading ZEN artifacts from remote bucket ===" && echo "" + + cd "${base_dir}"/travis_files + echo "$travis_urls" > ./travis_urls.txt + sudo apt-get update + sudo apt-get -y --no-install-recommends install aria2 + aria2c -x16 -s16 -i ./travis_urls.txt --allow-overwrite=true --always-resume=true --auto-file-renaming=false +fi + + +# Step 4 +echo "" && echo "=== Checksum verification ===" && echo "" + +if shasum -a256 -c ./*.sha256; then + echo "Checksum verification passed." +else + fn_die "Error: checksum verification failed. Exiting ..." +fi + +# Step 5 +release_folder="zen-${zen_tag}-amd64" +if [ "$is_cached" = false ]; then + echo "" && echo "=== Extract artifacts from tar ===" && echo "" + tar_file="$(find "$(realpath ${base_dir}/travis_files/)" -type f -name "*.tar.gz")" + + mkdir -p "${base_dir}/src/${release_folder}" + tar -xzf "${tar_file}" -C "${base_dir}/src/${release_folder}" +fi + +# Step 6 +echo "" && echo "=== Verify git tag signed by allowlisted maintainer ===" && echo "" + +cd "${base_dir}/src/${release_folder}" +GNUPGHOME="$(mktemp -d 2>/dev/null || mktemp -d -t "GNUPGHOME")" +export GNUPGHOME +import_gpg_keys "${ZEN_REPO_MAINTAINER_KEYS}" +check_signed_tag "${zen_tag}" +( gpgconf --kill dirmngr || true ) +( gpgconf --kill gpg-agent || true ) +rm -rf "${GNUPGHOME:?}" +unset GNUPGHOME + +# Step 7 +echo "" && echo "=== Export BITCOINCLI, BITCOIND and SIDECHAIN_SDK path as env vars, needed for python tests ===" && echo "" + +BITCOINCLI="${base_dir}/src/${release_folder}/src/zen-cli" +BITCOIND="${base_dir}/src/${release_folder}/src/zend" +SIDECHAIN_SDK="${CURRENT_DIR}" + +if [[ ! -f "$BITCOINCLI" ]]; then + fn_die "Error: zen-cli does not exist in the given path. Exiting ..." +fi +if [[ ! -f "$BITCOIND" ]]; then + fn_die "Error: zend does not exist in the given path. Exiting ..." +fi +if [[ ! -d "$SIDECHAIN_SDK" ]]; then + fn_die "Error: Sidechain-SDK does not exist in the given path. Exiting ..." +fi + +export BITCOINCLI +export BITCOIND +export SIDECHAIN_SDK + +# Step 8 +echo "" && echo "=== Fetch zen params ===" && echo "" +${base_dir}/src/${release_folder}/zcutil/fetch-params.sh || { retval="$?"; echo "Error: was not able to fetch zen params."; exit $retval; } + +# Step 9 +echo "" && echo "=== Building SideChain SDK ===" && echo "" +cd $CURRENT_DIR +mvn clean install -Dmaven.test.skip=true || { retval="$?"; echo "Error: was not able to complete mvn clean install of Sidechain SDK."; exit $retval; } + +# Step 10 +echo "" && echo "=== Installing nodejs ===" && echo "" +source ~/.nvm/nvm.sh +nvm install "${NODE_VERSION}" || { retval="$?"; echo "Error: was not able to nvm install node ${NODE_VERSION}"; exit $retval; } + +# Step 11 +echo "" && echo "=== Installing yarn ===" && echo "" +npm install --global yarn || { retval="$?"; echo "Error: was not able to install yarn with npm install."; exit $retval; } + +# Step 12 +echo "" && echo "=== Installing Python dependencies ===" && echo "" +pip install --no-cache-dir --upgrade pip +pip install --no-cache-dir -r ./requirements.txt +cd qa/ +pip install --no-cache-dir -r ./SidechainTestFramework/account/requirements.txt + +# Step 13 +echo "" && echo "=== Run tests ===" && echo "" +"${TEST_CMD}" "${TEST_ARGS}" \ No newline at end of file diff --git a/ci/run_sc.sh b/ci/run_sc.sh index 8685a34fde..6cf190c561 100755 --- a/ci/run_sc.sh +++ b/ci/run_sc.sh @@ -2,7 +2,7 @@ set -eo pipefail -SIMPLE_APP_VERSION="${SIMPLE_APP_VERSION:-0.8.0}" +SIMPLE_APP_VERSION="${SIMPLE_APP_VERSION:-0.9.0}" if [ -d "$1" ] && [ -f "$2" ]; then path_to_jemalloc="$(ldconfig -p | grep "$(arch)" | grep 'libjemalloc\.so\.1$' | tr -d ' ' | cut -d '>' -f 2)" diff --git a/coverage-reports/generate_report.sh b/coverage-reports/generate_report.sh new file mode 100755 index 0000000000..1fe731c3f7 --- /dev/null +++ b/coverage-reports/generate_report.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# this script generates jacoco code coverage report +# before running it, it is necessary to have the following env vars set - $SIDECHAIN_SDK, $BITCOIND and $BITCOINCLI +# additionally, it is necessary to have org.jacoco.cli-0.8.9-nodeps.jar which can be found here - https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/0.8.9/ +# this script should be run in the root of coverage-reports folder + +# Specify snapshot version +SNAPSHOT_VERSION_TAG="0.9.0" + +# Check if SIDECHAIN_SDK is set and not empty +if [ -z "$SIDECHAIN_SDK" ]; then + echo "Error: SIDECHAIN_SDK is not set or is empty. Please set the environment variable." + exit 1 +fi + +# Step 1: Change to the project directory +echo "Changing to the project directory..." +cd ../sdk || exit 1 + +# Step 2: Run 'mvn clean install' in the project directory +# it is necessary to let test phase run as well, because this phase creates .exec file for this code coverage report +echo "Running 'mvn clean install' in the project directory..." +mvn clean install + +# Check if BITCOIND and BITCOINCLI are set and not empty before running python tests +if [ -n "$BITCOIND" ] && [ -n "$BITCOINCLI" ]; then + # Step 3: Execute run_sc_tests.sh + # this phase runs python tests which append to the previously created .exec file to get the full code coverage report + echo "Executing script for integration tests..." + cd ../qa || exit 1 + ./run_sc_tests.sh -jacoco +else + echo "Warning: Either BITCOIND or BITCOINCLI is not set or is empty. Please set both environment variables." +fi + +# Step 3: Execute run_sc_tests.sh +# this phase runs python tests which append to the previously created .exec file to get the full code coverage report +echo "Executing script for integration tests..." +cd ../qa || exit 1 +./run_sc_tests.sh -jacoco + +# Step 4: Generate the JaCoCo code coverage report +# this phase creates the detailed report with html files for easier browsing +echo "Generating JaCoCo code coverage report..." + +JACOCO_JAR_PATH="$HOME/.m2/repository/org/jacoco/org.jacoco.cli/0.8.9/org.jacoco.cli-0.8.9-nodeps.jar" +EXEC_PATH="$SIDECHAIN_SDK/coverage-reports/sidechains-sdk-${SNAPSHOT_VERSION_TAG}/sidechains-sdk-${SNAPSHOT_VERSION_TAG}-jacoco-report.exec" +CLASSFILES_PATH="$SIDECHAIN_SDK/sdk/target/classes" +HTML_PATH="$SIDECHAIN_SDK/coverage-reports/sidechains-sdk-${SNAPSHOT_VERSION_TAG}/sidechains-sdk-${SNAPSHOT_VERSION_TAG}-jacoco-report" + +java -jar "$JACOCO_JAR_PATH" \ + report "$EXEC_PATH" \ + --classfiles "$CLASSFILES_PATH" \ + --html "$HTML_PATH" + +echo "Code coverage report generation complete." diff --git a/doc/index.md b/doc/index.md index 6e558b759e..b51ee1d642 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,3 +1,6 @@ # Horizen Sidechain SDK Release Notes -## Version [0.8.0](/doc/release/0.8.0.md) \ No newline at end of file +## Version [0.9.0](/doc/release/0.9.0.md) +## Version [0.8.1](/doc/release/0.8.1.md) +## Version [0.8.0](/doc/release/0.8.0.md) + diff --git a/doc/release/0.8.0.md b/doc/release/0.8.0.md index 189b370609..b0fd936f89 100644 --- a/doc/release/0.8.0.md +++ b/doc/release/0.8.0.md @@ -1,4 +1,5 @@ # Release notes - version 0.8.0 + --- ## Notes about new/updated Features @@ -28,7 +29,7 @@ sparkz { ``` ### Forgers Network -This feature adds a dedicated connection pool reserved for connecting to forger nodes. The size of the dedicated connection pool is configured by `maxForgerConnections` option in `network` section of configuration file. The default value is 20. This limit is independent of +This feature adds a dedicated connection pool reserved for connecting to forger nodes. The size of the dedicated connection pool is configured by `maxForgerConnections` option in `network` section of configuration file. The default value is 20. This limit works in addition to the one defined in the property network.maxOutgoingConnections. Configuration option `isForgerNode` in the `network` section has to be set to `true` to indicate that the node is a forger and other nodes should prioritize connecting with it. Default value is `false`. @@ -66,4 +67,4 @@ The intent of this feature is to minimize block propagation time between forgers - other performance improvements --- -Full [Changelog](/CHANGELOG.md) file here \ No newline at end of file +Full [Changelog](/CHANGELOG.md) file here diff --git a/doc/release/0.8.1.md b/doc/release/0.8.1.md new file mode 100644 index 0000000000..b9220a149c --- /dev/null +++ b/doc/release/0.8.1.md @@ -0,0 +1,13 @@ +# Release notes - version 0.8.1 + +--- + +## Notes about new/updated Features + +eth RPC call eth_gasPrice now returns an estimation of the gasPrice more accurate.\ +The previous one was overestimating the minimum gasPrice needed in some corner cases.\ +Following the new endpoint estimation, will be possible to pay lower average fees than before.\ +The update will be immediately visible, not requirying an hardfork activation. + +--- +Full [Changelog](/CHANGELOG.md) file here diff --git a/doc/release/0.9.0.md b/doc/release/0.9.0.md new file mode 100644 index 0000000000..0a5df6fc39 --- /dev/null +++ b/doc/release/0.9.0.md @@ -0,0 +1,69 @@ +# Release notes - version 0.9.0 + +--- + +## Notes about new/updated Features + +### Native Smart Contracts <> EVM Smart Contracts Interoperability +It is now possible for an EVM Smart Contract to invoke a Native Smart Contract function and vice-versa. +The EVM Smart Contract can invoke a Native Smart Contract function using the following low-level functions: +- `staticcall` +- `call` + +`delegatecall` and `callcode` cannot be used instead, they will return an error in case they are used with Native +Smart Contracts. + +In addition, it is possible to use the Solidity interface describing the Native Smart Contract. +Files with the Native Smart Contract Solidity interfaces can be found under: +`qa/SidechainTestFramework/account/smart_contract_resources/contracts/`. + +Example of Native Smart Contract invocation in Solidity using low-level `staticcall` function: + +``` + // ... +address contractAddr = address(0x0000000000000000000022222222222222222222); +(bool success, bytes memory result) = contractAddr.staticcall{gas: 100000}( + abi.encodeWithSignature("getAllForgersStakes()") +); + + // ... +``` + +Example using Solidity interface: + +``` +import "./ForgerStakes.sol"; + + // ... + ForgerStakes nativeContract = ForgerStakes(0x0000000000000000000022222222222222222222); + nativeContract.getAllForgersStakes{gas: 100000}(); + + // ... + +``` + + + + + +--- +## Update instructions from previous version + +--- +## Bug Fixes + + +--- + +## Improvements + + + +--- +## Update test instructions from previous version + +- Install Python version 3.10 + + +--- +Full [Changelog](/CHANGELOG.md) file here diff --git a/examples/README.md b/examples/README.md index f033d497f7..baca3bf873 100644 --- a/examples/README.md +++ b/examples/README.md @@ -44,24 +44,24 @@ Otherwise, to run an Example App outside the IDE: * (Windows) ``` cd Sidechains-SDK\examples\simpleapp - java -cp ./target/sidechains-sdk-simpleapp-0.8.1.jar;./target/lib/* io.horizen.examples.SimpleApp + java -cp ./target/sidechains-sdk-simpleapp-0.9.0.jar;./target/lib/* io.horizen.examples.SimpleApp ``` * (Linux) ``` cd ./Sidechains-SDK/examples/utxo/simpleapp - java -cp ./target/sidechains-sdk-simpleapp-0.8.1.jar:./target/lib/\* io.horizen.examples.SimpleApp + java -cp ./target/sidechains-sdk-simpleapp-0.9.0.jar:./target/lib/\* io.horizen.examples.SimpleApp ``` **Model: Account** * (Windows) ``` cd Sidechains-SDK\examples\evmapp - java -cp ./target/sidechains-sdk-evmapp-0.8.1.jar;./target/lib/* io.horizen.examples.EvmApp + java -cp ./target/sidechains-sdk-evmapp-0.9.0.jar;./target/lib/* io.horizen.examples.EvmApp ``` * (Linux) ``` cd ./Sidechains-SDK/examples/account/evmapp - java -cp ./target/sidechains-evmapp-0.8.1.jar:./target/lib/\* io.horizen.examples.EvmApp + java -cp ./target/sidechains-evmapp-0.9.0.jar:./target/lib/\* io.horizen.examples.EvmApp ``` On some Linux OSs during backward transfers certificates proofs generation an extremely large RAM consumption may happen, that will lead to the process being force killed by the OS. @@ -74,7 +74,7 @@ While we keep monitoring the memory footprint of the proofs generation process, - After the installation, just run `export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1` before starting the sidechain node, or run the sidechain node adding `LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1` at the beginning of the java command line as follows: ``` - LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 java -cp ./target/sidechains-sdk-simpleapp-0.8.1.jar:./target/lib/* io.horizen.examples.SimpleApp + LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.1 java -cp ./target/sidechains-sdk-simpleapp-0.9.0.jar:./target/lib/* io.horizen.examples.SimpleApp ``` - In the folder `ci` you will find the script `run_sc.sh` to automatically check and use jemalloc library while starting the sidechain node. diff --git a/examples/account/evmapp/pom.xml b/examples/account/evmapp/pom.xml index 1fac2070d1..426afbe1e4 100644 --- a/examples/account/evmapp/pom.xml +++ b/examples/account/evmapp/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-evmapp - 0.8.1 + 0.9.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.8.1 + 0.9.0 junit diff --git a/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfigurator.java b/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfigurator.java index f6b08f7796..907dabc945 100644 --- a/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfigurator.java +++ b/examples/account/evmapp/src/main/java/io/horizen/examples/AppForkConfigurator.java @@ -1,5 +1,6 @@ package io.horizen.examples; +import io.horizen.account.fork.ContractInteroperabilityFork; import io.horizen.fork.*; import io.horizen.account.fork.GasFeeFork; import io.horizen.account.fork.ZenDAOFork; @@ -62,7 +63,12 @@ public List> getOptiona new ActiveSlotCoefficientFork( 0.05 ) - ) + ), + new Pair<>( + // TODO the actual fork point needs to be decided + new SidechainForkConsensusEpoch(50, 50, 50), + new ContractInteroperabilityFork(true) + ) ); } } diff --git a/examples/account/evmapp/src/main/java/io/horizen/examples/EvmApp.java b/examples/account/evmapp/src/main/java/io/horizen/examples/EvmApp.java index c37a2c9861..2ed946e33a 100644 --- a/examples/account/evmapp/src/main/java/io/horizen/examples/EvmApp.java +++ b/examples/account/evmapp/src/main/java/io/horizen/examples/EvmApp.java @@ -19,9 +19,19 @@ public static void main(String[] args) { System.out.println("File on path " + args[0] + " doesn't exist"); return; } + + int mcBlockReferenceDelay = 0; + try { + if (args.length >= 2) { + mcBlockReferenceDelay = Integer.parseInt(args[1]); + } + } catch (NumberFormatException ex) { + System.out.println("MC Block Reference delay can not be parsed."); + } + String settingsFileName = args[0]; - Injector injector = Guice.createInjector(new EvmAppModule(settingsFileName)); + Injector injector = Guice.createInjector(new EvmAppModule(settingsFileName, mcBlockReferenceDelay)); AccountSidechainApp sidechainApp = injector.getInstance(AccountSidechainApp.class); Logger logger = LogManager.getLogger(EvmApp.class); diff --git a/examples/account/evmapp/src/main/java/io/horizen/examples/EvmAppModule.java b/examples/account/evmapp/src/main/java/io/horizen/examples/EvmAppModule.java index e64a1d08b8..35d97566f3 100644 --- a/examples/account/evmapp/src/main/java/io/horizen/examples/EvmAppModule.java +++ b/examples/account/evmapp/src/main/java/io/horizen/examples/EvmAppModule.java @@ -26,8 +26,15 @@ public class EvmAppModule extends AccountAppModule { private final SettingsReader settingsReader; - public EvmAppModule(String userSettingsFileName) { + // It's integer parameter that defines Mainchain Block Reference delay. + // 1 or 2 should be enough to avoid SC block reverting in the most cases. + // WARNING. It must be constant and should not be changed inside Sidechain network + private final int mcBlockRefDelay; + + + public EvmAppModule(String userSettingsFileName, int mcBlockDelayReference) { this.settingsReader = new SettingsReader(userSettingsFileName, Optional.empty()); + this.mcBlockRefDelay = mcBlockDelayReference; } @Override @@ -61,10 +68,6 @@ public void configureApp() { String appVersion = ""; - // It's integer parameter that defines Mainchain Block Reference delay. - // 1 or 2 should be enough to avoid SC block reverting in the most cases. - int mcBlockReferenceDelay = 0; - // use a custom object which implements the stopAll() method SidechainAppStopper applicationStopper = new EvmAppStopper(); @@ -109,6 +112,6 @@ public void configureApp() { .toInstance(appVersion); bind(Integer.class) .annotatedWith(Names.named("MainchainBlockReferenceDelay")) - .toInstance(mcBlockReferenceDelay); + .toInstance(mcBlockRefDelay); } } diff --git a/examples/account/evmapp_sctool/pom.xml b/examples/account/evmapp_sctool/pom.xml index ed2f1d1840..556ec651cc 100644 --- a/examples/account/evmapp_sctool/pom.xml +++ b/examples/account/evmapp_sctool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-evmapp_sctool - 0.8.1 + 0.9.0 2022 11 @@ -14,13 +14,13 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.8.1 + 0.9.0 compile io.horizen sidechains-sdk-evmapp - 0.8.1 + 0.9.0 compile diff --git a/examples/mc_sc_workflow_example.md b/examples/mc_sc_workflow_example.md index 784a1a1424..b2dbb5738e 100644 --- a/examples/mc_sc_workflow_example.md +++ b/examples/mc_sc_workflow_example.md @@ -32,8 +32,8 @@ Build SDK components by using a command (in the root of the Sidechains-SDK folde Run Bootstrapping tool using the command depending on the sidechain model: -- account: `java -jar tools/sidechains-sdk-account_sctools/target/sidechains-sdk-account_sctools-0.8.1.jar` -- utxo: `java -jar tools/sidechains-sdk-utxo_sctools/target/sidechains-sdk-utxo_sctools-0.8.1.jar` +- account: `java -jar tools/sidechains-sdk-account_sctools/target/sidechains-sdk-account_sctools-0.9.0.jar` +- utxo: `java -jar tools/sidechains-sdk-utxo_sctools/target/sidechains-sdk-utxo_sctools-0.9.0.jar` All other commands are performed as commands for Bootstrapping tool in the next format: `"command name" "parameters for command in JSON format"`. For any help, you could use the command `help`, for the exit just print `exit` @@ -124,7 +124,7 @@ For retrieving other Schnorr keys repeat this command. The creation of Sidechains requires data for proving backward transfer operations, included in certificates. *If the circuit without key rotation, that data could be generated by the next command:* -`generateCertProofInfo {"signersPublicKeys": [pk1, pk2, ...], "threshold": 5, "verificationKeyPath": "/tmp/sidechainapp/cert_marlin_snark_vk", "provingKeyPath": "/tmp/sidechainapp/cert_marlin_snark_pk", "isCSWEnabled": true}` +`generateCertProofInfo {"signersPublicKeys": ["pk1", "pk2", ...], "threshold": 1, "verificationKeyPath": "/tmp/sidechainapp/cert_marlin_snark_vk", "provingKeyPath": "/tmp/sidechainapp/cert_marlin_snark_pk", "isCSWEnabled": true}` Note: - `signersPublicKeys` - list of Schnorr public keys of certificate Signers generated on step 5; @@ -133,7 +133,7 @@ Note: *If circuit with key rotation:* -`generateCertWithKeyRotationProofInfo {"signersPublicKeys": [signerPk1, signerPk2, ...], "mastersPublicKeys": [masterPk1, masterPk2, ...], "threshold": 5, "verificationKeyPath": "/tmp/sidechainapp/cert_marlin_snark_vk", "provingKeyPath": "/tmp/sidechainapp/cert_marlin_snark_pk"}` +`generateCertWithKeyRotationProofInfo {"signersPublicKeys": ["signerPk1", "signerPk2", ...], "mastersPublicKeys": ["masterPk1", "masterPk2", ...], "threshold": 5, "verificationKeyPath": "/tmp/sidechainapp/cert_marlin_snark_vk", "provingKeyPath": "/tmp/sidechainapp/cert_marlin_snark_pk", "isCSWEnabled":false}` Note: - `signersPublicKeys` - list of Schnorr public signing keys of certificate Signers generated on step 5; @@ -166,7 +166,7 @@ Save all outputs from previous steps and type `exit` for exit from Bootstrapping Compile MC sources with SC support code: 1. Clone Horizen core repository - https://github.com/HorizenOfficial/zen/ -2. Use the main SC support branch - `master` +2. Use the main SC support branch - `main` 3. Build the Core for your platform using the guides in the repo. **Step 8: Setup local zen node** @@ -507,30 +507,30 @@ Run an Example App with the `my_settings.conf`: * For Windows: ``` - java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.8.1.jar;./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf + java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.9.0.jar;./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf ``` * For Linux (Glibc): ``` - java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.8.1.jar:./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf + java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.9.0.jar:./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf ``` * For Linux (Jemalloc): ``` - LD_PRELOAD=/libjemalloc.so.1 java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.8.1.jar:./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf + LD_PRELOAD=/libjemalloc.so.1 java -cp ./examples/utxo/simpleapp/target/sidechains-sdk-simpleapp-0.9.0.jar:./examples/simpleapp/target/lib/* io.horizen.examples.SimpleApp ./examples/my_settings.conf ``` **Model: Account** * For Windows: ``` - java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.8.1.jar;./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf + java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.9.0.jar;./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf ``` * For Linux (Glibc): ``` - java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.8.1.jar:./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf + java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.9.0.jar:./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf ``` * For Linux (Jemalloc): ``` - LD_PRELOAD=/libjemalloc.so.1 java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.8.1.jar:./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf + LD_PRELOAD=/libjemalloc.so.1 java -cp ./examples/account/evmapp/target/sidechains-sdk-evmapp-0.9.0.jar:./examples/evmapp/target/lib/* io.horizen.examples.EvmApp ./examples/my_settings.conf ``` diff --git a/examples/utxo/simpleapp/pom.xml b/examples/utxo/simpleapp/pom.xml index 406ee2248d..7445473682 100644 --- a/examples/utxo/simpleapp/pom.xml +++ b/examples/utxo/simpleapp/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-simpleapp - 0.8.1 + 0.9.0 2018 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.8.1 + 0.9.0 junit diff --git a/examples/utxo/simpleapp/src/main/java/io/horizen/examples/SimpleAppModule.java b/examples/utxo/simpleapp/src/main/java/io/horizen/examples/SimpleAppModule.java index 075954c24e..714b70be64 100644 --- a/examples/utxo/simpleapp/src/main/java/io/horizen/examples/SimpleAppModule.java +++ b/examples/utxo/simpleapp/src/main/java/io/horizen/examples/SimpleAppModule.java @@ -7,7 +7,6 @@ import java.util.Optional; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; -import io.horizen.fork.ConsensusParamsFork; import io.horizen.utxo.SidechainAppModule; import io.horizen.SidechainAppStopper; import io.horizen.SidechainSettings; @@ -45,6 +44,7 @@ public SimpleAppModule(String userSettingsFileName, int mcBlockDelayReference) { public void configureApp() { SidechainSettings sidechainSettings = this.settingsReader.getSidechainSettings(); + int maxHistoryRewritingLength = 200; HashMap>> customBoxSerializers = new HashMap<>(); HashMap> customSecretSerializers = new HashMap<>(); @@ -55,18 +55,15 @@ public void configureApp() { //Initialize the App Fork Configurator AppForkConfigurator forkConfigurator = new AppForkConfigurator(); - //Get the max number of consensus slots per epoch from the App Fork configurator and use it to set the Storage versions to mantain - int maxConsensusEpoch = ConsensusParamsFork.getMaxPossibleSlotsEver(forkConfigurator.getOptionalSidechainForks()); - // two distinct storages are used in application state and wallet in order to test a version // misalignment during startup and the recover logic File appWalletStorage1 = new File(dataDirAbsolutePath + "/appWallet1"); File appWalletStorage2 = new File(dataDirAbsolutePath + "/appWallet2"); - DefaultApplicationWallet defaultApplicationWallet = new DefaultApplicationWallet(appWalletStorage1, appWalletStorage2, maxConsensusEpoch * 2 + 1); + DefaultApplicationWallet defaultApplicationWallet = new DefaultApplicationWallet(appWalletStorage1, appWalletStorage2, maxHistoryRewritingLength); File appStateStorage1 = new File(dataDirAbsolutePath + "/appState1"); File appStateStorage2 = new File(dataDirAbsolutePath + "/appState2"); - DefaultApplicationState defaultApplicationState = new DefaultApplicationState(appStateStorage1, appStateStorage2, maxConsensusEpoch * 2 + 1); + DefaultApplicationState defaultApplicationState = new DefaultApplicationState(appStateStorage1, appStateStorage2, maxHistoryRewritingLength); File secretStore = new File(dataDirAbsolutePath + "/secret"); File walletBoxStore = new File(dataDirAbsolutePath + "/wallet"); @@ -119,37 +116,37 @@ public void configureApp() { bind(Storage.class) .annotatedWith(Names.named("SecretStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(secretStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(secretStore, 5)); bind(Storage.class) .annotatedWith(Names.named("WalletBoxStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(walletBoxStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(walletBoxStore, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("WalletTransactionStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(walletTransactionStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(walletTransactionStore, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("WalletForgingBoxesInfoStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(walletForgingBoxesInfoStorage, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(walletForgingBoxesInfoStorage, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("WalletCswDataStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(walletCswDataStorage, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(walletCswDataStorage, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("StateStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(stateStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(stateStore, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("StateForgerBoxStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(stateForgerBoxStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(stateForgerBoxStore, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("StateUtxoMerkleTreeStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(stateUtxoMerkleTreeStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(stateUtxoMerkleTreeStore, maxHistoryRewritingLength)); bind(Storage.class) .annotatedWith(Names.named("HistoryStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(historyStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(historyStore, 5)); bind(Storage.class) .annotatedWith(Names.named("ConsensusStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(consensusStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(consensusStore, 5)); bind(Storage.class) .annotatedWith(Names.named("BackupStorage")) - .toInstance(new VersionedLevelDbStorageAdapter(backupStore, maxConsensusEpoch * 2 + 1)); + .toInstance(new VersionedLevelDbStorageAdapter(backupStore, maxHistoryRewritingLength)); bind(new TypeLiteral> () {}) .annotatedWith(Names.named("CustomApiGroups")) diff --git a/examples/utxo/utxoapp_sctool/pom.xml b/examples/utxo/utxoapp_sctool/pom.xml index 48cca98e36..862715925b 100644 --- a/examples/utxo/utxoapp_sctool/pom.xml +++ b/examples/utxo/utxoapp_sctool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-utxoapp_sctool - 0.8.1 + 0.9.0 2018 11 @@ -14,13 +14,13 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.8.1 + 0.9.0 compile io.horizen sidechains-sdk-simpleapp - 0.8.1 + 0.9.0 compile diff --git a/pom.xml b/pom.xml index 4d1b56778c..14f5568d94 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen Sidechains - 0.8.1 + 0.9.0 2018 UTF-8 diff --git a/qa/README.md b/qa/README.md index 86bf8fde45..aed3dc50b3 100644 --- a/qa/README.md +++ b/qa/README.md @@ -7,7 +7,7 @@ It is possible to test a SC node or nodes with or without real MC node connectio **Requirements** -- Install Python 3.5 or newer +- Install Python 3.10 or newer ``` sudo apt install python sudo apt-get -y install python3-pip diff --git a/qa/SidechainTestFramework/account/ac_use_smart_contract.py b/qa/SidechainTestFramework/account/ac_use_smart_contract.py index ad393de6d0..bf768d4f2e 100644 --- a/qa/SidechainTestFramework/account/ac_use_smart_contract.py +++ b/qa/SidechainTestFramework/account/ac_use_smart_contract.py @@ -52,7 +52,7 @@ def __init__(self, contract_path: str): SmartContract.__load( SmartContract.__find_contract_data_path( SmartContract.__split_path( - contract_path.rstrip(".sol"))))) + contract_path.removesuffix(".sol"))))) self.Functions = dict() for obj in self.Abi: if obj['type'] == 'function': @@ -385,7 +385,7 @@ def __load(json_file: str): @staticmethod def __split_path(contr: str): - return contr.rstrip('.sol').split('/')[-1].split('\\')[-1] + return contr.removesuffix('.sol').split('/')[-1].split('\\')[-1] @staticmethod def __get_input_type(inp: dict): @@ -412,7 +412,7 @@ def __find_contract_data_path(contr: str): sol_files = [] for root, __, files in os.walk(base): for file in files: - if file.endswith(".json") and not file.endswith(".dbg.json") and file.rstrip(".json") == contr: + if file.endswith(".json") and not file.endswith(".dbg.json") and file.removesuffix(".json") == contr: sol_files.append(os.path.join(root, file)) if len(sol_files) < 1: raise RuntimeError( diff --git a/qa/SidechainTestFramework/account/ac_utils.py b/qa/SidechainTestFramework/account/ac_utils.py index 323d4f05dd..2cfeca69a1 100644 --- a/qa/SidechainTestFramework/account/ac_utils.py +++ b/qa/SidechainTestFramework/account/ac_utils.py @@ -280,3 +280,20 @@ def ac_makeForgerStake(sc_node, owner_address, blockSignPubKey, vrf_public_key, } return sc_node.transaction_makeForgerStake(json.dumps(forgerStakes)) + + +def ac_invokeProxy(sc_node, contract_address, data, nonce=None, static=False): + params = { + "invokeInfo": { + "contractAddress": contract_address, + "dataStr": data + }, + "nonce": nonce + } + + if (static): + return sc_node.transaction_invokeProxyStaticCall(json.dumps(params)) + else: + return sc_node.transaction_invokeProxyCall(json.dumps(params)) + + diff --git a/qa/SidechainTestFramework/account/requirements.txt b/qa/SidechainTestFramework/account/requirements.txt index daa025535d..85c2178e9b 100644 --- a/qa/SidechainTestFramework/account/requirements.txt +++ b/qa/SidechainTestFramework/account/requirements.txt @@ -4,3 +4,4 @@ rlp eth_utils eth_bloom base58 +requests \ No newline at end of file diff --git a/qa/SidechainTestFramework/account/simple_proxy_contract.py b/qa/SidechainTestFramework/account/simple_proxy_contract.py new file mode 100644 index 0000000000..a93613527a --- /dev/null +++ b/qa/SidechainTestFramework/account/simple_proxy_contract.py @@ -0,0 +1,136 @@ +import logging + +from eth_typing import HexStr +from eth_utils import add_0x_prefix + +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import deploy_smart_contract, format_eoa +from test_framework.util import hex_str_to_bytes + + +class SimpleProxyContract: + call_sig = "doCall(address,uint256,bytes)" + static_call_sig = "doStaticCall(address,bytes)" + + def __init__(self, sc_node, evm_address): + logging.info(f"Creating smart contract utilities for SimpleProxy") + self.sc_node = sc_node + self.contract = SmartContract("SimpleProxy") + logging.info(self.contract) + self.contract_address = deploy_smart_contract(sc_node, self.contract, evm_address, None, '', True) + + def do_call(self, from_address, nonce, target_addr, value, data): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + data_input = self.contract.raw_encode_call(self.call_sig, target_addr, value, + data) + result = self.sc_node.rpc_eth_call( + { + "to": self.contract_address, + "from": add_0x_prefix(from_address), + "nonce": nonce, + "input": data_input + }, "latest" + ) + + if "result" in result: + raw_res = self.contract.raw_decode_call_result(self.call_sig, + hex_str_to_bytes(format_eoa(result['result']))) + return raw_res[0] + + raise RuntimeError("Something went wrong, see {}".format(str(result))) + + def do_call_trace(self, from_address, nonce, target_addr, value, data): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + data_input = self.contract.raw_encode_call(self.call_sig, target_addr, value, + data) + result = self.sc_node.rpc_debug_traceCall( + { + "to": self.contract_address, + "from": add_0x_prefix(from_address), + "nonce": nonce, + "input": data_input, + }, "latest", { + "tracer": "callTracer" + } + ) + + return result + + def do_static_call(self, from_address, nonce, target_addr, data): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + data_input = self.contract.raw_encode_call(self.static_call_sig, target_addr, data) + result = self.sc_node.rpc_eth_call( + { + "to": self.contract_address, + "from": add_0x_prefix(from_address), + "nonce": nonce, + "input": data_input + }, "latest" + ) + + if "result" in result: + raw_res = self.contract.raw_decode_call_result(self.static_call_sig, + hex_str_to_bytes(format_eoa(result['result']))) + return raw_res[0] + + raise RuntimeError("Something went wrong, see {}".format(str(result))) + + def do_static_call_trace(self, from_address, nonce, target_addr, data): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + data_input = self.contract.raw_encode_call(self.static_call_sig, target_addr, + data) + result = self.sc_node.rpc_debug_traceCall( + { + "to": self.contract_address, + "from": add_0x_prefix(from_address), + "nonce": nonce, + "input": data_input + }, "latest", { + "tracer": "callTracer" + } + ) + + return result + + def call_transaction(self, from_add, nonce: int, target_addr, value, data, gas_limit=20000000): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + tx_id = self.contract.call_function(self.sc_node, self.call_sig, target_addr, + value, data, fromAddress=from_add, + toAddress=format_eoa(self.contract_address), nonce=nonce, + gasLimit=gas_limit + ) + + return tx_id + + def estimate_gas(self, from_add, nonce: int, target_addr, value, data, gas_limit=20000000): + if isinstance(data, str): + data = hex_str_to_bytes(data) + elif not isinstance(data, bytes): + raise Exception("Only valid data types are byte arrays or string") + data_input = self.contract.raw_encode_call(self.call_sig, target_addr, value, + data) + + request = { + "from": add_0x_prefix(from_add), + "to": self.contract_address, + "data": data_input, + "nonce": nonce, + "gas": HexStr(hex(gas_limit)) + } + + return self.sc_node.rpc_eth_estimateGas(request) diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/CertKeyRotation.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/CertKeyRotation.sol new file mode 100644 index 0000000000..d4b0fb8a62 --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/CertKeyRotation.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + + +// contract address: 0x0000000000000000000044444444444444444444 +interface CertKeyRotation { + + function submitKeyRotation(uint32 key_type, uint32 index, bytes32 newKey_1, bytes1 newKey_2, bytes32 signKeySig_1, bytes32 signKeySig_2, bytes32 masterKeySig_1, bytes32 masterKeySig_2, bytes32 newKeySig_1, bytes32 newKeySig_2) external returns (uint32, uint32, bytes32, bytes1, bytes32, bytes32, bytes32, bytes32); +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol new file mode 100644 index 0000000000..8c8aa5edd6 --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/ForgerStakes.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +type StakeID is bytes32; + +// contract address: 0x0000000000000000000022222222222222222222 +interface ForgerStakes { + + struct StakeInfo { + StakeID stakeId; + uint256 stakedAmount; + address owner; + bytes32 publicKey; + bytes32 vrf1; + bytes1 vrf2; + } + + function getAllForgersStakes() external view returns (StakeInfo[] memory); + + function delegate(bytes32 publicKey, bytes32 vrf1, bytes1 vrf2, address owner) external payable returns (StakeID); + + function withdraw(StakeID stakeId, bytes1 signatureV, bytes32 signatureR, bytes32 signatureS) external returns (StakeID); + + function openStakeForgerList(uint32 forgerIndex, bytes32 signature1, bytes32 signature2) external returns (bytes memory); +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/McAddrOwnership.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/McAddrOwnership.sol new file mode 100644 index 0000000000..757c37ccc8 --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/McAddrOwnership.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + + +// contract address: 0x0000000000000000000088888888888888888888 +interface McAddrOwnership { + + struct McAddrOwnershipData { + address scAddress; + bytes3 mcAddrBytes1; + bytes32 mcAddrBytes2; + } + + function getAllKeyOwnerships() external view returns (McAddrOwnershipData[] memory); + + function getKeyOwnerships(address scAddress) external view returns (McAddrOwnershipData[] memory); + + function getKeyOwnerScAddresses() external view returns (address[] memory); + + function sendKeysOwnership(bytes3 mcAddrBytes1, bytes32 mcAddrBytes2, bytes24 signature1, bytes32 signature2, bytes32 signature3) external returns (bytes32); + + function removeKeysOwnership(bytes3 mcAddrBytes1, bytes32 mcAddrBytes2) external returns (bytes32); +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/NativeInterop.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/NativeInterop.sol new file mode 100644 index 0000000000..b33207aeef --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/NativeInterop.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; + +import "./ForgerStakes.sol"; + +contract NativeInterop { + ForgerStakes nativeContract = ForgerStakes(0x0000000000000000000022222222222222222222); + + function GetForgerStakes() public view returns (ForgerStakes.StakeInfo[] memory) { + // set an explicit gas limit of 10000 for this call for the unit test + return nativeContract.getAllForgersStakes{gas: 100000}(); + } + + function GetForgerStakesDelegateCall() public { + // This call does not really make sense as the storage layout of this contract does not match the + // forger stakes contracts at all. It is only here because it should immediately throw an error. + (bool success, bytes memory result) = address(nativeContract).delegatecall( + abi.encodeWithSignature("getAllForgersStakes()") + ); + if (success == false) { + assembly { + revert(add(result, 32), mload(result)) + } + } + } + + function GetForgerStakesCallCode() public { + // This call does not really make sense as the storage layout of this contract does not match the + // forger stakes contracts at all. It is only here because it should immediately throw an error. + // NOTE: we can't directly use "callcode" anymore as the solidity compiler deprecated it long ago and + // will not compile this anymore +// (bool success, bytes memory data) = address(nativeContract).callcode( +// abi.encodeWithSignature("getAllForgersStakes()") +// ); + // using inline assembly CALLCODE can still be used + address contractAddr = address(nativeContract); + // function signature + bytes4 sig = bytes4(keccak256("getAllForgersStakes()")); + assembly { + let x := mload(0x40) //Find empty storage location using "free memory pointer" + mstore(x, sig) //Place signature at beginning of empty storage + mstore(0x40, add(x, 0x04)) // set free pointer before function call. so it is used by called function. + // new free pointer position after the output values of the called function. + + let success := callcode( + 10000, //10 gas + contractAddr, //To addr + 0, //No wei passed + x, // Inputs are at location x + 0x04, //Inputs size, just the signature, so 4 bytes + x, //Store output over input - never used, throws before this + 0x20 //Output is 32 bytes long - never used, throws before this + ) + } + } +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/SimpleProxy.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/SimpleProxy.sol new file mode 100644 index 0000000000..bc8c9e9c1d --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/SimpleProxy.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +contract SimpleProxy { + + + function doStaticCall(address to, bytes calldata data) public returns (bytes memory) { + uint256 gasRemaining = gasleft(); + (bool success, bytes memory result) = to.staticcall{gas:gasRemaining}( + data + ); + require(success, "staticcall should work"); + return result; + } + + + function doCall(address to, uint value, bytes calldata data) public returns (bytes memory) { + uint256 gasRemaining = gasleft(); + (bool success, bytes memory result) = to.call{value:value, gas:gasRemaining}( + data + ); + require(success, "call should work"); + return result; + } + + fallback() external payable { + } + +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/Storage.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/Storage.sol new file mode 100644 index 0000000000..2100177846 --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/Storage.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + */ +contract Storage { + + uint256 number; + + // Event declaration + // Up to 3 parameters can be indexed. + // Indexed parameters helps you filter the logs by the indexed parameter + event Log(address indexed sender, string message); + event AnotherLog(); + + constructor(uint256 initialNumber) { + number = initialNumber; + emit Log(msg.sender, "Hello World!"); + emit Log(msg.sender, "Hello EVM!"); + emit AnotherLog(); + } + + function inc() public { + number = number + 1; + } + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public payable { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} diff --git a/qa/SidechainTestFramework/account/smart_contract_resources/contracts/WithdrawalRequests.sol b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/WithdrawalRequests.sol new file mode 100644 index 0000000000..7e7eb8782c --- /dev/null +++ b/qa/SidechainTestFramework/account/smart_contract_resources/contracts/WithdrawalRequests.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +type MCAddress is bytes20; + +// contract address: 0x0000000000000000000011111111111111111111 +interface WithdrawalRequests { + + struct WithdrawalRequest { + MCAddress mcAddress; + uint256 value; + } + + function getBackwardTransfers(uint32 withdrawalEpoch) external view returns (WithdrawalRequest[] memory); + + function backwardTransfer(MCAddress mcAddress) external payable returns (WithdrawalRequest memory); +} diff --git a/qa/SidechainTestFramework/account/utils.py b/qa/SidechainTestFramework/account/utils.py index ef1374c0b9..43d2b01ef1 100644 --- a/qa/SidechainTestFramework/account/utils.py +++ b/qa/SidechainTestFramework/account/utils.py @@ -38,9 +38,16 @@ def convertZenniesToZen(valueInZennies): FORGER_STAKE_SMART_CONTRACT_ADDRESS = "0000000000000000000022222222222222222222" CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS = "0000000000000000000044444444444444444444" MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS = "0000000000000000000088888888888888888888" +PROXY_SMART_CONTRACT_ADDRESS = "00000000000000000000AAAAAAAAAAAAAAAAAAAA" # address used for burning coins NULL_ADDRESS = "0000000000000000000000000000000000000000" +# TODO It may change. It should have the same value as in src/main/java/io/horizen/examples/AppForkConfigurator.java +INTEROPERABILITY_FORK_EPOCH = 50 + +# The activation epoch of the zendao feature, as coded in the sdk +ZENDAO_FORK_EPOCH = 7 + # Block gas limit BLOCK_GAS_LIMIT = 30000000 diff --git a/qa/SidechainTestFramework/sc_test_framework.py b/qa/SidechainTestFramework/sc_test_framework.py index 4897164f79..858401a9f8 100644 --- a/qa/SidechainTestFramework/sc_test_framework.py +++ b/qa/SidechainTestFramework/sc_test_framework.py @@ -11,7 +11,7 @@ from SidechainTestFramework.scutil import initialize_default_sc_chain_clean, \ start_sc_nodes, stop_sc_nodes, \ sync_sc_blocks, sync_sc_mempools, TimeoutException, bootstrap_sidechain_nodes, APP_LEVEL_INFO, set_sc_parallel_test, \ - SNAPSHOT_VERSION_TAG + SNAPSHOT_VERSION_TAG, set_jacoco import os import tempfile import traceback @@ -186,6 +186,8 @@ def main(self): help="Stores parallel process integer assigned to current test") parser.add_option("--mcblockdelay", dest="mcblockdelay", type=int, default=0, action="store", help="Stores mainchain block delay reference parameter") + parser.add_option("--jacoco", dest="jacoco", default=False, action="store_true", + help="Stores jacoco flag assigned to current test") self.add_options(parser) self.sc_add_options(parser) @@ -209,6 +211,8 @@ def main(self): if parallel_group > 0: self.set_parallel_test(parallel_group) + set_jacoco(self.options.jacoco) + self.setup_chain() self.setup_network() diff --git a/qa/SidechainTestFramework/scutil.py b/qa/SidechainTestFramework/scutil.py index a7eaafd88d..8a37126a6f 100755 --- a/qa/SidechainTestFramework/scutil.py +++ b/qa/SidechainTestFramework/scutil.py @@ -19,7 +19,7 @@ WAIT_CONST = 1 -SNAPSHOT_VERSION_TAG = "0.8.1" +SNAPSHOT_VERSION_TAG = "0.9.0" # log levels of the log4j trace system used by java applications APP_LEVEL_OFF = "off" @@ -57,6 +57,9 @@ # Parallel Testing parallel_test = 0 +# flag for jacoco code coverage analysis +is_jacoco_included = False + class TimeoutException(Exception): def __init__(self, operation): @@ -74,6 +77,9 @@ def set_sc_parallel_test(n): global parallel_test parallel_test = n +def set_jacoco(value): + global is_jacoco_included + is_jacoco_included = value def start_port_modifier(): if parallel_test > 0: @@ -691,7 +697,17 @@ def start_sc_node(i, dirname, extra_args=None, rpchost=None, timewait=None, bina Currently, it is permitted by default and a warning is issued. The --add-opens VM option remove this warning. ''' - bashcmd = 'java --add-opens java.base/java.lang=ALL-UNNAMED ' + dbg_agent_opt + ' -cp ' + binary + " " + cfgFileName \ + + user_home = os.path.expanduser("~") + jacoco_agent_path = os.path.join(user_home, ".m2", "repository", "org", "jacoco", "org.jacoco.agent", "0.8.9", + "org.jacoco.agent-0.8.9-runtime.jar") + jacoco_cmd = f'-javaagent:{jacoco_agent_path}=destfile=../coverage-reports/sidechains-sdk-{SNAPSHOT_VERSION_TAG}/sidechains-sdk-{SNAPSHOT_VERSION_TAG}-jacoco-report.exec,append=true' + + if is_jacoco_included: + bashcmd = 'java --add-opens java.base/java.lang=ALL-UNNAMED ' + jacoco_cmd + dbg_agent_opt + ' -cp ' + binary + " " + cfgFileName \ + + " " + mc_block_delay_ref + else: + bashcmd = 'java --add-opens java.base/java.lang=ALL-UNNAMED ' + dbg_agent_opt + ' -cp ' + binary + " " + cfgFileName \ + " " + mc_block_delay_ref if print_output_to_file: diff --git a/qa/mc_sc_evm_forging1_with_mc_block_delay.py b/qa/mc_sc_evm_forging1_with_mc_block_delay.py new file mode 100755 index 0000000000..3e8648c941 --- /dev/null +++ b/qa/mc_sc_evm_forging1_with_mc_block_delay.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +import os + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, SCCreationInfo, MCConnectionInfo, \ + SCNetworkConfiguration, LARGE_WITHDRAWAL_EPOCH_LENGTH, SC_CREATION_VERSION_2, KEY_ROTATION_CIRCUIT +from test_framework.util import initialize_chain_clean, start_nodes, \ + websocket_port_by_mc_node_index, connect_nodes_bi, disconnect_nodes_bi +from SidechainTestFramework.scutil import bootstrap_sidechain_nodes, start_sc_nodes, generate_next_blocks, AccountModel, \ + EVM_APP_BINARY +from SidechainTestFramework.sc_forging_util import * + +""" +Check forger behavior for: +1. Current SC Tip with no new MC data extension. +2. Current SC Tip with some new MC References extension. +3. Current SC Tip with some new MC Headers and/or MC Ref Data extension. +4. Not a SC Tip, because of MC fork, linear ommers inclusion. +5. Not a SC Tip, because of MC fork, recursive ommers inclusion. + +Configuration: + Start 3 MC nodes and 1 SC node (with default websocket configuration). + SC node connected to the first MC node. + MC nodes are connected. + MC block reference delay is 1 + +Test: + - Synchronize MC nodes to the point of SC Creation Block. + - Disconnect MC nodes. + - Forge SC block, verify that there is no MC Headers and Data, no ommers. + - Mine MC block on MC node 1 and forge SC block respectively, verify SC block has no MC data inclusion. + - Mine MC block on MC node 1 and forge SC block respectively, verify MC data inclusion. + - Mine 3 MC blocks on MC node 2. Connect and synchronize MC nodes 1 and 2. + - Forge SC block, verify that previously forged block was set as ommer, verify MC data inclusion. + - Mine 2 MC blocks in MC node 1. + - Forge SC block, verify MC data inclusion. + - Forge one more SC block, verify MC data inclusion. + - Forge one more SC block, verify that there is no MC data, no ommers. + - Mine 6 Mc block in MC node 3. Connect and synchronize MC node 1 and 3. + - Forge SC block, verify that previously forged blocks were set as ommers, verify MC data inclusion. + - Check SC node forging status + + MC blocks on MC node 1 in the end: + 420 - 421 - 422 + \ + - 421' - 422' - 423' - 424' + \ + - 421'' - 422'' - 423'' - 424'' - 425''* + + + SC Block on SC node in the end: [; ; ] + G[420h;420d;] - 0[;;] - 1[421h;421d;] + \ + - 2[421'h,422'h;;1[...]] - 3[423'h,424'h;421'd-424'd;] - 4[;;] + \ + - 5[421''h-425''h;;2[...;1],3[...],4[;;]] - 6[;421''-425'';] +""" + + +class MCSCEvmForging1(AccountChainSetup): + def __init__(self): + super().__init__(number_of_sidechain_nodes=1, + number_of_mc_nodes=3 + ) + + def setup_chain(self): + initialize_chain_clean(self.options.tmpdir, self.number_of_mc_nodes) + + def setup_network(self, split = False): + # Setup nodes and connect them + self.nodes = self.setup_nodes() + connect_nodes_bi(self.nodes, 0, 1) + connect_nodes_bi(self.nodes, 0, 2) + self.sync_all() + + def setup_nodes(self): + # Start 3 MC nodes + return start_nodes(self.number_of_mc_nodes, self.options.tmpdir) + + def sc_setup_nodes(self): + return start_sc_nodes(self.number_of_sidechain_nodes, dirname=self.options.tmpdir,extra_args=[['-mc_block_delay_ref', '1']], + binary=[EVM_APP_BINARY] * self.number_of_sidechain_nodes + ) + + def sc_setup_chain(self): + # Bootstrap new SC, specify SC node 1 connection to MC node 1 + mc_node_1 = self.nodes[0] + sc_node_1_configuration = SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node_1.hostname, websocket_port_by_mc_node_index(0))) + ) + + network = SCNetworkConfiguration(SCCreationInfo(mc_node_1, 600, LARGE_WITHDRAWAL_EPOCH_LENGTH), + sc_node_1_configuration) + bootstrap_sidechain_nodes(self.options, network, model=AccountModel) + + def run_test(self): + # Synchronize mc_node1, mc_node2 and mc_node3, then disconnect them. + self.sync_all() + disconnect_nodes_bi(self.nodes, 0, 1) + disconnect_nodes_bi(self.nodes, 0, 2) + mc_node1 = self.nodes[0] + mc_node2 = self.nodes[1] + mc_node3 = self.nodes[2] + sc_node1 = self.sc_nodes[0] + + mcblock_hash0 = mc_node1.getbestblockhash() + + # Test 1: Generate SC block, when all MC blocks already synchronized. + # Generate 1 SC block + scblock_id0 = generate_next_blocks(sc_node1, "first node", 1)[0] + # Verify that SC block has no MC headers, ref data, ommers + check_mcheaders_amount(0, scblock_id0, sc_node1) + check_mcreferencedata_amount(0, scblock_id0, sc_node1) + check_ommers_amount(0, scblock_id0, sc_node1) + + # Test 2: Generate SC block, when new MC blocks following the same Tip appear. + # Generate 1 MC block on the first MC node + mcblock_hash1 = mc_node1.generate(1)[0] + # Generate 1 SC block + scblock_id1 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id0, scblock_id1, sc_node1) + # Verify that SC block contains MC block as a Mainchain Header + check_mcheaders_amount(0, scblock_id1, sc_node1) + check_mcreferencedata_amount(0, scblock_id1, sc_node1) + + # Generate 1 MC block on the first MC node + mcblock_hash2 = mc_node1.generate(1)[0] + scblock_id2 = generate_next_blocks(sc_node1, "first node", 1)[0] + # Generate 1 SC block + + check_mcheaders_amount(1, scblock_id2, sc_node1) + check_mcreferencedata_amount(1, scblock_id2, sc_node1) + check_mcreference_presence(mcblock_hash1, scblock_id2, sc_node1) + check_ommers_amount(0, scblock_id1, sc_node1) + + # Test 3: Generate SC block, when new MC blocks following different Tip appear. Ommers expected. + # Generate another 3 MC blocks on the second MC node + fork_mcblock_hash1 = mc_node2.generate(1)[0] + fork_mcblock_hash2 = mc_node2.generate(1)[0] + fork_mcblock_hash3 = mc_node2.generate(1)[0] + + # Connect and synchronize MC node 1 to MC node 2 + connect_nodes_bi(self.nodes, 0, 1) + self.sync_nodes([mc_node1, mc_node2]) + # MC Node 1 should replace mcblock_hash1 Tip with [fork_mcblock_hash1, fork_mcblock_hash2, fork_mcblock_hash3] + assert_equal(fork_mcblock_hash3, mc_node1.getbestblockhash()) + + # Generate 1 SC block + scblock_id3 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id1, scblock_id3, sc_node1) + # Verify that SC block contains newly created MC blocks has a MainchainHeaders and no MainchainRefData + check_mcheaders_amount(2, scblock_id3, sc_node1) + check_mcreferencedata_amount(0, scblock_id3, sc_node1) + check_mcheader_presence(fork_mcblock_hash1, scblock_id3, sc_node1) + check_mcheader_presence(fork_mcblock_hash2, scblock_id3, sc_node1) + # Verify that SC block contains 1 Ommer with 1 MainchainHeader + check_ommers_amount(1, scblock_id3, sc_node1) + check_ommers_cumulative_score(1, scblock_id3, sc_node1) + check_ommer(scblock_id2, [mcblock_hash1], scblock_id3, sc_node1) + + # Test 4: Generate SC block, when new MC blocks following the same Tip appear + 2 previous RefData expecting to be synchronized. + # Generate 2 more mc blocks in MC node 1 + mcblock_hash3 = mc_node1.generate(1)[0] + mcblock_hash4 = mc_node1.generate(1)[0] + + # Generate SC block + scblock_id4 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id3, scblock_id4, sc_node1) + # Verify that SC block MainchainHeaders and MainchainRefData + check_mcheaders_amount(2, scblock_id4, sc_node1) + check_mcreferencedata_amount(4, scblock_id4, sc_node1) + check_mcheader_presence(fork_mcblock_hash3, scblock_id4, sc_node1) + check_mcheader_presence(mcblock_hash3, scblock_id4, sc_node1) + # check_mcheader_presence(mcblock_hash4, scblock_id3, sc_node1) + check_mcreferencedata_presence(fork_mcblock_hash1, scblock_id4, sc_node1) + check_mcreferencedata_presence(fork_mcblock_hash2, scblock_id4, sc_node1) + check_mcreferencedata_presence(fork_mcblock_hash3, scblock_id4, sc_node1) + check_mcreferencedata_presence(mcblock_hash3, scblock_id4, sc_node1) + check_ommers_amount(0, scblock_id4, sc_node1) + + # Generate SC block with no MC data. Needed for further test + scblock_id5 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id4, scblock_id5, sc_node1) + check_mcheaders_amount(0, scblock_id5, sc_node1) + check_mcreferencedata_amount(0, scblock_id5, sc_node1) + check_ommers_amount(0, scblock_id5, sc_node1) + + # Test 5: MC Node 3 generates MC blocks, that hust from sc creation tx containing block. + # After MC synchronization, SC node should create with recursive ommers. + # Generate another 6 blocks on MC node 3 + another_fork_mcblocks_hashes = mc_node3.generate(6) + another_fork_tip_hash = another_fork_mcblocks_hashes[-1] + connect_nodes_bi(self.nodes, 0, 2) + self.sync_all() + # MC Node 1 should replace mcblock_hash4 Tip with another_fork_tip_hash + assert_equal(another_fork_tip_hash, mc_node1.getbestblockhash()) + + # Generate SC block + scblock_id6 = generate_next_blocks(sc_node1, "first node", 1)[0] + # logging.info(json.dumps(sc_node1.block_findById(blockId=scblock_id5), indent=4)) + check_scparent(scblock_id1, scblock_id6, sc_node1) + # Verify that SC block contains newly created MC blocks as a MainchainHeaders and no MainchainRefData + check_mcheaders_amount(5, scblock_id6, sc_node1) + check_mcreferencedata_amount(0, scblock_id6, sc_node1) + for mchash in another_fork_mcblocks_hashes[:-1]: + check_mcheader_presence(mchash, scblock_id6, sc_node1) + # Verify that SC block contains 3 Ommers + check_ommers_amount(3, scblock_id6, sc_node1) + # Verify Ommers cumulative score, that must also count 1 subommer + check_ommers_cumulative_score(4, scblock_id6, sc_node1) + expected_ommers_ids = [scblock_id3, scblock_id4, scblock_id5] + for ommer_id in expected_ommers_ids: + check_ommer(ommer_id, [], scblock_id6, sc_node1) + check_subommer(scblock_id3, scblock_id2, [mcblock_hash1], scblock_id6, sc_node1) + + # Generate 1 more SC Block to sync MainchainRefData + scblock_id7 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id6, scblock_id7, sc_node1) + # Verify that SC block contains newly created MC blocks as a MainchainRefData and no MainchainHeaders + check_mcheaders_amount(0, scblock_id7, sc_node1) + check_mcreferencedata_amount(5, scblock_id7, sc_node1) + check_ommers_amount(0, scblock_id7, sc_node1) + for mchash in another_fork_mcblocks_hashes[:5]: + check_mcreferencedata_presence(mchash, scblock_id7, sc_node1) + + # Check SC node forging status + # Auto forging is disabled. + is_forging_enabled = sc_node1.block_forgingInfo()["result"]["forgingEnabled"] + assert_equal(False, is_forging_enabled, "Automatic forging expected to be disabled.") + # Enable forging + if "result" not in sc_node1.block_startForging(): + fail("Was not able to start auto forging.") + # Check the new status + is_forging_enabled = sc_node1.block_forgingInfo()["result"]["forgingEnabled"] + assert_equal(True, is_forging_enabled, "Automatic forging expected to be enabled.") + + +if __name__ == "__main__": + MCSCEvmForging1().main() diff --git a/qa/mc_sc_evm_forging3_with_mc_block_delay.py b/qa/mc_sc_evm_forging3_with_mc_block_delay.py new file mode 100755 index 0000000000..2c0d6034c1 --- /dev/null +++ b/qa/mc_sc_evm_forging3_with_mc_block_delay.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, SCCreationInfo, MCConnectionInfo, \ + SCNetworkConfiguration, LARGE_WITHDRAWAL_EPOCH_LENGTH +from SidechainTestFramework.sc_forging_util import * +from SidechainTestFramework.scutil import bootstrap_sidechain_nodes, start_sc_nodes, generate_next_blocks, AccountModel, \ + EVM_APP_BINARY +from test_framework.util import initialize_chain_clean, start_nodes, \ + websocket_port_by_mc_node_index, connect_nodes_bi, disconnect_nodes_bi + +""" +Check Latus forger behavior for: +1. Sidechain block with recursive ommers to the same mc branch inclusion: mainchain fork races. + +Configuration: + Start 3 MC nodes and 1 SC node (with default websocket configuration). + SC node connected to the first MC node. + MC nodes are connected. + MC block reference delay is 1 + +Test: + - Synchronize MC nodes to the point of SC Creation Block. + - Disconnect MC nodes. + - Forge SC block, verify that there is no MC Headers and Data, no ommers. + - Mine 2 MC blocks on MC node 1, sync with MC node 3, then forge SC block respectively, verify MC data inclusion. + - Mine 3 MC blocks on MC node 2. Connect and synchronize MC nodes 1 and 2. Fork became an active chain. + - Forge SC block, verify that previously forged block was set as ommer, verify MC data inclusion. + - Mine 2 MC blocks in MC node 3, sync again with MC Node 1. Previous chain is active again. + - Forge SC block, verify MC data inclusion and ommers/subommers inclusion. + + MC blocks on MC node 1 in the end: + 420 - 421 - 422 - 423* + \ + - 421' - 422' - 423' + + + SC Block on SC node in the end: [; ; ] + G[420h;420d;] - 0[;;] - 1[421h;421d;] + \ + - 2[421'h,422'h;;1[...]] + \ + - 3[421h,422h,423h;;2[...;1]] +""" + + +class MCSCEvmForging3(AccountChainSetup): + def __init__(self): + super().__init__(number_of_sidechain_nodes=1, + number_of_mc_nodes=3 + ) + + def setup_chain(self): + initialize_chain_clean(self.options.tmpdir, self.number_of_mc_nodes) + + def setup_network(self, split = False): + # Setup nodes and connect them + self.nodes = self.setup_nodes() + connect_nodes_bi(self.nodes, 0, 1) + connect_nodes_bi(self.nodes, 0, 2) + self.sync_all() + + def setup_nodes(self): + # Start 3 MC nodes + return start_nodes(self.number_of_mc_nodes, self.options.tmpdir) + + def sc_setup_chain(self): + # Bootstrap new SC, specify SC node 1 connection to MC node 1 + mc_node_1 = self.nodes[0] + sc_node_1_configuration = SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node_1.hostname, websocket_port_by_mc_node_index(0))) + ) + + network = SCNetworkConfiguration(SCCreationInfo(mc_node_1, 600, LARGE_WITHDRAWAL_EPOCH_LENGTH), + sc_node_1_configuration) + bootstrap_sidechain_nodes(self.options, network, model=AccountModel) + + def sc_setup_nodes(self): + # Start 1 SC node + return start_sc_nodes(self.number_of_sidechain_nodes, dirname=self.options.tmpdir, + extra_args=[['-mc_block_delay_ref', '1']], + binary=[EVM_APP_BINARY] * self.number_of_sidechain_nodes + ) + + def run_test(self): + # Synchronize mc_node1, mc_node2 and mc_node3, then disconnect them. + self.sync_all() + disconnect_nodes_bi(self.nodes, 0, 1) + disconnect_nodes_bi(self.nodes, 0, 2) + mc_node1 = self.nodes[0] + mc_node2 = self.nodes[1] + mc_node3 = self.nodes[2] + sc_node1 = self.sc_nodes[0] + + + # Test 1: Generate SC block, when all MC blocks already synchronized. + # Generate 1 SC block + scblock_id0 = generate_next_blocks(sc_node1, "first node", 1)[0] + # Verify that SC block has no MC headers, ref data, ommers + check_mcheaders_amount(0, scblock_id0, sc_node1) + check_mcreferencedata_amount(0, scblock_id0, sc_node1) + check_ommers_amount(0, scblock_id0, sc_node1) + + + # Test 2: Generate SC block, when new MC block following the same Tip appear. + # Generate 1 MC block on the first MC node + mcblock_hash1 = mc_node1.generate(1)[0] + mcblock_hash2 = mc_node1.generate(1)[0] + + # Sync MC nodes 1 and 3 once + connect_nodes_bi(self.nodes, 0, 2) + self.sync_nodes([mc_node1, mc_node3]) + disconnect_nodes_bi(self.nodes, 0, 2) + + # Generate 1 SC block + scblock_id1 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id0, scblock_id1, sc_node1) + # Verify that SC block contains MC block as a MainchainReference + check_mcheaders_amount(1, scblock_id1, sc_node1) + check_mcheader_presence(mcblock_hash1, scblock_id1, sc_node1) + check_mcreferencedata_amount(1, scblock_id1, sc_node1) + check_mcreferencedata_presence(mcblock_hash1, scblock_id1, sc_node1) + check_ommers_amount(0, scblock_id1, sc_node1) + + + # Test 3: Generate SC block, when new MC blocks following different Tip appear. Ommers expected. + # Generate another 2 MC blocks on the second MC node + fork_mcblock_hash1 = mc_node2.generate(1)[0] + fork_mcblock_hash2 = mc_node2.generate(1)[0] + fork_mcblock_hash3 = mc_node2.generate(1)[0] + + # Connect and synchronize MC node 1 to MC node 2 + connect_nodes_bi(self.nodes, 0, 1) + self.sync_nodes([mc_node1, mc_node2]) + # MC Node 1 should replace mcblock_hash1 Tip with [fork_mcblock_hash1, fork_mcblock_hash2] + assert_equal(fork_mcblock_hash3, mc_node1.getbestblockhash()) + + # Generate 1 SC block + scblock_id2 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id0, scblock_id2, sc_node1) + # Verify that SC block contains newly created MC blocks as a MainchainHeaders and no MainchainRefData + check_mcheaders_amount(2, scblock_id2, sc_node1) + check_mcreferencedata_amount(0, scblock_id2, sc_node1) + check_mcheader_presence(fork_mcblock_hash1, scblock_id2, sc_node1) + check_mcheader_presence(fork_mcblock_hash2, scblock_id2, sc_node1) + # Verify that SC block contains 1 Ommer with 1 MainchainHeader + check_ommers_amount(1, scblock_id2, sc_node1) + check_ommers_cumulative_score(1, scblock_id2, sc_node1) + check_ommer(scblock_id1, [mcblock_hash1], scblock_id2, sc_node1) + + + # Test 4: Generate SC block, when new MC blocks following previous Tip appear and lead to chain switching again. + # Ommers expected. Subommers expected with mc blocks for the same MC branch as current SC block, + # but orphaned to parent Ommer MC headers. + + # Generate 2 more mc blocks in MC node 3 + mcblock_hash3 = mc_node3.generate(1)[0] + mcblock_hash4 = mc_node3.generate(1)[0] + + # Sync MC nodes 1 and 3 once + connect_nodes_bi(self.nodes, 0, 2) + self.sync_nodes([mc_node1, mc_node3]) + disconnect_nodes_bi(self.nodes, 0, 2) + # MC Node 1 should replace back fork_mcblock_hash2 Tip with [mcblock_hash1, mcblock_hash2, mcblock_hash3, mcblock_hash4] + assert_equal(mcblock_hash4, mc_node1.getbestblockhash()) + + # Generate SC block + scblock_id3 = generate_next_blocks(sc_node1, "first node", 1)[0] + check_scparent(scblock_id0, scblock_id3, sc_node1) + # Verify that SC block contains newly created MC blocks as a MainchainHeaders and no MainchainRefData + check_mcheaders_amount(3, scblock_id3, sc_node1) + check_mcreferencedata_amount(0, scblock_id3, sc_node1) + check_mcheader_presence(mcblock_hash1, scblock_id3, sc_node1) + check_mcheader_presence(mcblock_hash2, scblock_id3, sc_node1) + check_mcheader_presence(mcblock_hash3, scblock_id3, sc_node1) + # Verify Ommers cumulative score, that must also count 1 subommer + check_ommers_cumulative_score(2, scblock_id3, sc_node1) + # Verify that SC block contains 1 Ommer with 2 MainchainHeader + check_ommers_amount(1, scblock_id3, sc_node1) + check_ommer(scblock_id2, [fork_mcblock_hash1, fork_mcblock_hash2], scblock_id3, sc_node1) + # Verify that Ommer contains 1 subommer with 1 MainchainHeader + check_subommer(scblock_id2, scblock_id1, [mcblock_hash1], scblock_id3, sc_node1) + + +if __name__ == "__main__": + MCSCEvmForging3().main() \ No newline at end of file diff --git a/qa/mc_sc_evm_forging4_with_mc_block_delay.py b/qa/mc_sc_evm_forging4_with_mc_block_delay.py new file mode 100755 index 0000000000..c872acbf24 --- /dev/null +++ b/qa/mc_sc_evm_forging4_with_mc_block_delay.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +import os + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, SCCreationInfo, MCConnectionInfo, \ + SCNetworkConfiguration, LARGE_WITHDRAWAL_EPOCH_LENGTH +from test_framework.util import initialize_chain_clean, start_nodes, \ + websocket_port_by_mc_node_index +from SidechainTestFramework.scutil import bootstrap_sidechain_nodes, start_sc_nodes, generate_next_blocks, AccountModel, \ + EVM_APP_BINARY +from SidechainTestFramework.sc_forging_util import * + + +""" +Check Latus forger behavior for: +1. Sidechain has multiple MC blocks to be synchronized (>50). Check that sidechains forging not fails due to + time out(SC create too many requests to MC for block generation). + +Configuration: + Start 1 MC nodes and 1 SC node. + SC node connected to the first MC node. + MC block reference delay is 1 + +Test: + - Synchronize MC nodes to the point of SC Creation Block. + - Mine 1 MC block, then forge 1 SC block, verify MC hash inclusion + - Mine 200 MC blocks, then forge 5 SC blocks, verify MC data inclusion + + TODO In tests when SC is more than 50 blocks beyond MC Forger cannot retrieve more than 49 block headers + for forging one block. Update this test after modifying Forger with bigger headers amount. +""" + + +class MCSCEvmForging4(AccountChainSetup): + + inclusion_per_block = 48 # This value depends on amount of block hashes Sidechain can retrieve from the Mainchain + # in one request and MC Block Reference which reduces number of MC Headers to include + + def __init__(self): + super().__init__(number_of_sidechain_nodes=1, + number_of_mc_nodes=1 + ) + + def setup_chain(self): + initialize_chain_clean(self.options.tmpdir, self.number_of_mc_nodes) + + def setup_network(self, split = False): + # Setup nodes and connect them + self.nodes = self.setup_nodes() + self.sync_all() + + def setup_nodes(self): + # Start MC node + return start_nodes(self.number_of_mc_nodes, self.options.tmpdir) + + def sc_setup_chain(self): + # Bootstrap new SC, specify SC node 1 connection to MC node 1 + mc_node = self.nodes[0] + sc_node_configuration = SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node.hostname, websocket_port_by_mc_node_index(0))) + ) + + network = SCNetworkConfiguration(SCCreationInfo(mc_node, 600, LARGE_WITHDRAWAL_EPOCH_LENGTH), + sc_node_configuration) + bootstrap_sidechain_nodes(self.options, network, model=AccountModel) + + def sc_setup_nodes(self): + # Start 1 SC node + return start_sc_nodes(self.number_of_sidechain_nodes, dirname=self.options.tmpdir, + extra_args=[['-mc_block_delay_ref', '1']], + binary=[EVM_APP_BINARY] * self.number_of_sidechain_nodes + ) + + def run_test(self): + self.sync_all() + mc_node = self.nodes[0] + sc_node = self.sc_nodes[0] + + # Generate MC block. Hash of this block won't be included into first SC block(scblock_id0) + mcblock_id0 = mc_node.generate(1)[0] + # Generate SC block + scblock_id0 = generate_next_blocks(sc_node, "first node", 1)[0] + check_mcheaders_amount(0, scblock_id0, sc_node) + check_mcreferencedata_amount(0, scblock_id0, sc_node) + + + # Generate 200 MC blocks + mcblock_hashes = mc_node.generate(200) + included_mcblock_hashes = [mcblock_id0] + mcblock_hashes[:199] + # Generate 5 SC blocks + scblock_ids = generate_next_blocks(sc_node, "first node", 5) + + # Verify that SC block contains newly created MC blocks as MainchainHeaders and MainchainReferenceData + # First 4 SC blocks. Every block contains 48 MainchainHeaders and 48 MainchainReferenceData + for i in range(4): + check_mcheaders_amount(self.inclusion_per_block, scblock_ids[i], sc_node) + for mchash in included_mcblock_hashes[i * self.inclusion_per_block : (i + 1) * self.inclusion_per_block]: + check_mcheader_presence(mchash, scblock_ids[i], sc_node) + check_mcreferencedata_amount(self.inclusion_per_block, scblock_ids[i], sc_node) + for mchash in included_mcblock_hashes[i * self.inclusion_per_block : (i + 1) * self.inclusion_per_block]: + check_mcreferencedata_presence(mchash, scblock_ids[i], sc_node) + + # Fifth block. Contains 8 MainchainHeaders and 8 MainchainReferenceData + check_mcheaders_amount(8, scblock_ids[4], sc_node) + for mchash in included_mcblock_hashes[self.inclusion_per_block*4:200]: + check_mcheader_presence(mchash, scblock_ids[4], sc_node) + check_mcreferencedata_amount(8, scblock_ids[4], sc_node) + for mchash in included_mcblock_hashes[self.inclusion_per_block*4:200]: + check_mcreferencedata_presence(mchash, scblock_ids[4], sc_node) + + +if __name__ == "__main__": + MCSCEvmForging4().main() diff --git a/qa/run_sc_tests.sh b/qa/run_sc_tests.sh index 82188f3251..e4cfbb4983 100755 --- a/qa/run_sc_tests.sh +++ b/qa/run_sc_tests.sh @@ -34,6 +34,10 @@ for i in "$@"; do EVM_ONLY="true" shift ;; + -jacoco) + JACOCO="true" + shift + ;; -utxo_only) UTXO_ONLY="true" shift @@ -89,6 +93,7 @@ testScriptsEvm=( 'sc_evm_mempool_invalid_txs.py' 'sc_evm_node_info.py' 'sc_evm_orphan_txs.py' + 'sc_evm_native_interop.py' 'sc_evm_rpc_invalid_blocks.py' 'sc_evm_rpc_invalid_txs.py' 'sc_evm_rpc_net_methods.py' @@ -123,10 +128,17 @@ testScriptsEvm=( 'account_websocket_server_rpc.py' 'sc_evm_mc_addr_ownership.py' 'sc_evm_mc_addr_ownership_perf_test.py' + 'sc_evm_proxy_nsc.py' 'sc_evm_seedernode.py' 'sc_evm_consensus_parameters_fork.py' 'sc_evm_active_slot_coefficient.py' + 'mc_sc_evm_forging1_with_mc_block_delay.py' + 'mc_sc_evm_forging3_with_mc_block_delay.py' + 'mc_sc_evm_forging4_with_mc_block_delay.py' + 'sc_withdrawal_certificate_after_mainchain_nodes_were_disconnected.py' 'sc_evm_rpc_eth.py' + 'sc_evm_consensus_parameters_fork_with_sidechain_forks.py' + 'sc_evm_consensus_parameters_fork_with_mainchain_forks.py' ); testScriptsUtxo=( @@ -257,6 +269,16 @@ if [ ! -z "$EXCLUDE" ]; then done fi +# add --jacoco flag to each test if jacoco flag set to true +if [ ! -z "$JACOCO" ] && [ "${JACOCO}" = "true" ]; then + modifiedList=() + for testFile in "${testScripts[@]}"; do + modified_test="${testFile} --jacoco" + modifiedList+=("$modified_test") + done + testScripts=("${modifiedList[@]}") +fi + # split array into m parts and only run tests of part n where SPLIT=m:n if [ ! -z "$SPLIT" ]; then chunks="${SPLIT%*:*}" @@ -410,6 +432,7 @@ function runTestScript updateFailList="$testName" updateFailureCount echo "!!! FAIL: ${testName} !!! ### Run Time: $testRuntime(s) ###" | tee /dev/fd/3 + exit 1 fi echo | tee /dev/fd/3 diff --git a/qa/sc_backward_transfer.py b/qa/sc_backward_transfer.py index 731d211995..111cfe8f73 100755 --- a/qa/sc_backward_transfer.py +++ b/qa/sc_backward_transfer.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 -import json -import logging import time from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, SCCreationInfo, MCConnectionInfo, \ SCNetworkConfiguration, SC_CREATION_VERSION_1, SC_CREATION_VERSION_2, KEY_ROTATION_CIRCUIT +from SidechainTestFramework.sc_forging_util import * from SidechainTestFramework.sc_test_framework import SidechainTestFramework -from test_framework.util import fail, assert_equal, assert_false, start_nodes, \ - websocket_port_by_mc_node_index from SidechainTestFramework.scutil import bootstrap_sidechain_nodes, \ start_sc_nodes, check_box_balance, check_wallet_coins_balance, generate_next_blocks, generate_next_block -from SidechainTestFramework.sc_forging_util import * +from test_framework.util import assert_false, start_nodes, \ + websocket_port_by_mc_node_index """ Check Certificate automatic creation and submission to MC: diff --git a/qa/sc_evm_active_slot_coefficient.py b/qa/sc_evm_active_slot_coefficient.py old mode 100644 new mode 100755 diff --git a/qa/sc_evm_backward_transfer.py b/qa/sc_evm_backward_transfer.py index 622cd55de4..57c4c46319 100755 --- a/qa/sc_evm_backward_transfer.py +++ b/qa/sc_evm_backward_transfer.py @@ -1,24 +1,29 @@ #!/usr/bin/env python3 import logging import time +from _decimal import Decimal import base58 from eth_abi import decode from eth_utils import add_0x_prefix, encode_hex, event_signature_to_log_topic, remove_0x_prefix from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup -from SidechainTestFramework.account.ac_utils import generate_block_and_get_tx_receipt +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import generate_block_and_get_tx_receipt, format_eoa from SidechainTestFramework.account.httpCalls.transaction.allWithdrawRequests import all_withdrawal_requests +from SidechainTestFramework.account.httpCalls.transaction.createEIP1559Transaction import createEIP1559Transaction from SidechainTestFramework.account.httpCalls.transaction.withdrawCoins import withdrawcoins from SidechainTestFramework.account.httpCalls.wallet.balance import http_wallet_balance +from SidechainTestFramework.account.simple_proxy_contract import SimpleProxyContract from SidechainTestFramework.account.utils import (computeForgedTxFee, - convertZenToZennies, convertZenniesToWei) + convertZenToZennies, convertZenniesToWei, + WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, INTEROPERABILITY_FORK_EPOCH) from SidechainTestFramework.sc_forging_util import check_mcreference_presence, check_mcreferencedata_presence from SidechainTestFramework.scutil import ( - generate_next_block, generate_next_blocks + generate_next_block, generate_next_blocks, EVM_APP_SLOT_TIME ) from test_framework.util import ( - assert_equal, assert_false, hex_str_to_bytes, + assert_equal, assert_false, hex_str_to_bytes, bytes_to_hex_str, forward_transfer_to_sidechain, assert_true, fail, ) """ @@ -52,6 +57,7 @@ - reach next withdrawal epoch and verify that certificate for epoch 1 was added to MC mempool and then to MC/SC blocks. - verify epoch 1 certificate, verify backward transfers list + - interoperability tests """ @@ -77,7 +83,8 @@ def check_withdrawal_event(event, source_addr, dest_addr, amount, exp_epoch): class SCEvmBackwardTransfer(AccountChainSetup): def __init__(self): - super().__init__(withdrawalEpochLength=10) + super().__init__(block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH, + withdrawalEpochLength=10) def run_test(self): time.sleep(0.1) @@ -346,6 +353,147 @@ def run_test(self): assert_equal(we1_certHash, we1_sc_cert["hash"], "Certificate hash is different to the one in MC.") + ####################################################################################################### + # Interoperability test with an EVM smart contract calling backward transfer native contract + ####################################################################################################### + + # Create a new sc address to be used for the interoperability tests + evm_address_interop = sc_node.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + new_ft_amount_in_zen = Decimal('5.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_interop, + new_ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + + generate_next_block(sc_node, "first node") + + # Create and deploy evm proxy contract + proxy_contract = SimpleProxyContract(sc_node, evm_address_interop) + + native_contract = SmartContract("WithdrawalRequests") + + # Test before interoperability fork + method = "getBackwardTransfers(uint32)" + native_input = format_eoa(native_contract.raw_encode_call(method, current_epoch_number)) + try: + proxy_contract.do_static_call(evm_address_interop, 1, WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, native_input) + fail("Interoperability call should fail before fork point") + except RuntimeError as err: + print("Expected exception thrown: {}".format(err)) + # error is raised from API since the address has no balance + assert_true("reverted" in str(err)) + + # reach the Interoperability fork + current_best_epoch = sc_node.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + res = proxy_contract.do_static_call(evm_address_interop, 1, WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, + native_input) + + # res is (bytes20, uint256)[]. Its ABI encoding in this case is + # - first 32 bytes is the offset + # - second 32 bytes is array length + # - the remaining are the bytes representing the various (bytes20, uint256). Each element is formed of 64 bytes, + # 32 for bytes20, 32 for uint256 + res = res[32:] # cut offset, don't care in this case + num_of_wr = int(bytes_to_hex_str(res[0:32]), 16) + assert_equal(2, num_of_wr, "wrong number of backward transfer") + res = res[32:] # cut the array length + + elem_size = 64 # 32 + 32 because each elem is a tuple of bytes20 and uint256 + list_of_elems = [res[i:i + elem_size] for i in range(0, num_of_wr*elem_size, elem_size)] + + wr_1 = decode([('(bytes20,uint256)')], list_of_elems[0]) + wr_2 = decode([('(bytes20,uint256)')], list_of_elems[1]) + + assert_equal(convertZenniesToWei(sc_bt_amount_in_zennies_1), wr_1[0][1], "Wrong amount in wr") + mcDestAddr = bytes_to_hex_str(wr_1[0][0]) + + mc_address1_pk = base58.b58decode_check(mc_address1).hex()[4:] + assert_equal(mc_address1_pk, mcDestAddr,"Wrong mc address") + + assert_equal(convertZenniesToWei(sc_bt_amount_in_zennies_2), wr_2[0][1], "Wrong amount in wr") + + mcDestAddr = bytes_to_hex_str(wr_2[0][0]) + mc_address2_pk = base58.b58decode_check(mc_address2).hex()[4:] + assert_equal(mc_address2_pk, mcDestAddr, "Wrong mc address") + + # Create a backward transfer + # First, evm smart contract needs some zen to withdraw: + # 1) create a new sc address + # 2) ft some zen to new sc address + # 3) send some zen from new sc address to proxy smart contract + + createEIP1559Transaction(sc_node, fromAddress=evm_address_interop, toAddress=format_eoa( + proxy_contract.contract_address), + nonce=1, gasLimit=230000, maxPriorityFeePerGas=900000000, + maxFeePerGas=900000000, value=1000000000000) + generate_next_block(sc_node, "first node") + + # Create a transaction requesting a withdrawal request using the proxy smart contract + method = "backwardTransfer(bytes20)" + native_input = format_eoa(native_contract.raw_encode_call(method, hex_str_to_bytes(mc_address1_pk))) + + bt_amount_in_zennies = 100 + bt_amount_in_wei = convertZenniesToWei(bt_amount_in_zennies) + + # Estimate gas. The result will be compared with the actual used gas + exp_gas = proxy_contract.estimate_gas(evm_address_interop, 2, WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, + bt_amount_in_wei, native_input) + + logging.info("exp_gas: {}".format(exp_gas)) + + tx_id = proxy_contract.call_transaction(evm_address_interop, 2, WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, + bt_amount_in_wei, native_input) + receipt = generate_block_and_get_tx_receipt(sc_node, tx_id) + logging.info("receipt: {}".format(receipt)) + logging.info("gas used in receipt: {}".format(receipt['result']['gasUsed'])) + + # Check the status of tx + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Wrong tx status in receipt") + # Check the logs + assert_equal(1, len(receipt['result']['logs']), "Wrong number of events in receipt") + wr_event = receipt['result']['logs'][0] + check_withdrawal_event(wr_event, format_eoa(proxy_contract.contract_address), mc_address1, bt_amount_in_zennies, 2) + + # Compare estimated gas with actual used gas. They are not equal because, during the tx execution, more gas than + # actually needed is removed from the gas pool and then refunded. This causes the gas estimation algorithm to + # overestimate the gas. + gas_used = int(receipt['result']['gasUsed'], 16) + estimated_gas = int(exp_gas['result'], 16) + assert_true(estimated_gas >= gas_used, "Wrong estimated gas") + + # Check tracer + trace_response = sc_node.rpc_debug_traceTransaction(tx_id, {"tracer": "callTracer"}) + logging.info(trace_response) + + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_equal(elem_size * 2 + 2, len(native_call["output"])) # 130 = 64 bytes * 2 + 0x + assert_false("calls" in native_call) + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_true(gas_used == gas_used_tracer, "Wrong gas") + if __name__ == "__main__": SCEvmBackwardTransfer().main() diff --git a/qa/sc_evm_cert_key_rotation.py b/qa/sc_evm_cert_key_rotation.py index 2edc7f21d6..33abe7b605 100755 --- a/qa/sc_evm_cert_key_rotation.py +++ b/qa/sc_evm_cert_key_rotation.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import logging import time from binascii import hexlify @@ -7,19 +8,24 @@ from eth_utils import event_signature_to_log_topic, encode_hex from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import format_eoa from SidechainTestFramework.account.httpCalls.transaction.allWithdrawRequests import all_withdrawal_requests from SidechainTestFramework.account.httpCalls.transaction.createKeyRotationTransaction import \ http_create_key_rotation_transaction_evm +from SidechainTestFramework.account.simple_proxy_contract import SimpleProxyContract +from SidechainTestFramework.account.utils import CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS, \ + INTEROPERABILITY_FORK_EPOCH from SidechainTestFramework.sc_boostrap_info import KEY_ROTATION_CIRCUIT from SidechainTestFramework.sc_forging_util import * from SidechainTestFramework.scutil import generate_next_blocks, generate_next_block, generate_cert_signer_secrets, \ - get_withdrawal_epoch + get_withdrawal_epoch, EVM_APP_SLOT_TIME from SidechainTestFramework.secure_enclave_http_api_server import SecureEnclaveApiServer from httpCalls.submitter.getCertifiersKeys import http_get_certifiers_keys from httpCalls.submitter.getKeyRotationMessageToSign import http_get_key_rotation_message_to_sign_for_signing_key, \ http_get_key_rotation_message_to_sign_for_master_key from httpCalls.submitter.getKeyRotationProof import http_get_key_rotation_proof -from test_framework.util import assert_equal, assert_true, assert_false, hex_str_to_bytes +from test_framework.util import assert_equal, assert_true, assert_false, hex_str_to_bytes, forward_transfer_to_sidechain """ Configuration: @@ -55,6 +61,7 @@ - Call the getCertificateSigners endpoint and verify that all the signing and master keys are updated. ######## WITHDRAWAL EPOCH 4 ########## - Verify that certificate was created using all new keys + - Verify that CertificateKeyRotation native contract can be called by an EVM smart contract """ @@ -98,7 +105,8 @@ def __init__(self): # self.submitter_private_keys_indexes = list(range(self.cert_max_keys)) # self.cert_sig_threshold = 24 - super().__init__(withdrawalEpochLength=10, circuittype_override=KEY_ROTATION_CIRCUIT, + super().__init__(block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH, + withdrawalEpochLength=10, circuittype_override=KEY_ROTATION_CIRCUIT, remote_keys_manager_enabled=True, remote_keys_server_addresses=[self.remote_keys_address], cert_max_keys=self.cert_max_keys, cert_sig_threshold=self.cert_sig_threshold, submitters_private_keys_indexes=[self.submitter_private_keys_indexes]) @@ -154,7 +162,7 @@ def run_test(self): private_master_keys.append(new_signing_key_4.secret) public_master_keys.append(new_public_key_4) - # Change ALL the signing keys and ALL tee master keys + # Change ALL the signing keys and ALL the master keys new_signing_keys = [] new_master_keys = [] for i in range(self.cert_max_keys): @@ -572,7 +580,7 @@ def run_test(self): new_master_key_signature = self.secure_enclave_create_signature(message_to_sign=new_master_key_hash, key=new_m_key.secret)["signature"] - # Create the key rotation transacion to change the signing key + # Create the key rotation transaction to change the signing key response = http_create_key_rotation_transaction_evm(sc_node, key_type=0, key_index=i, @@ -583,7 +591,7 @@ def run_test(self): assert_false("error" in response) generate_next_blocks(sc_node, "first node", 1) - # Create the key rotation transacion to change the master key + # Create the key rotation transaction to change the master key response = http_create_key_rotation_transaction_evm(sc_node, key_type=1, key_index=i, @@ -672,6 +680,155 @@ def run_test(self): "Certificate Keys Root Hash incorrect") assert_equal(self.cert_max_keys, cert['quality'], "Certificate quality is wrong.") + ####################################################################################################### + # Interoperability test with an EVM smart contract calling Certificate Key Rotation native contract + ####################################################################################################### + time.sleep(1) + # Create and deploy evm proxy contract + # Create a new sc address to be used for the interoperability tests + evm_address_interop = sc_node.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + new_ft_amount_in_zen = 50 + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_interop, + new_ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + + generate_next_block(sc_node, "first node") + + # Deploy proxy contract + proxy_contract = SimpleProxyContract(sc_node, evm_address_interop) + # Create native contract interface, useful encoding/decoding params. Note this doesn't deploy a contract. + native_contract = SmartContract("CertKeyRotation") + + native_contract_address = CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS + + # Test 'submitKeyRotation(uint32,uint32,bytes32,bytes1,bytes32,bytes32,bytes32,bytes32,bytes32,bytes32)' + method = 'submitKeyRotation(uint32,uint32,bytes32,bytes1,bytes32,bytes32,bytes32,bytes32,bytes32,bytes32)' + + # Update again the signing key 0 + new_signing_key_interop = generate_cert_signer_secrets("random_seed_interop", 1, self.model)[0] + new_public_key_interop = new_signing_key_interop.publicKey + + epoch = get_withdrawal_epoch(sc_node) + + signing_key_message_interop = ( + http_get_key_rotation_message_to_sign_for_signing_key(sc_node, + new_public_key_interop, + epoch))["keyRotationMessageToSign"] + + # Prepare signatures + master_signature_interop = self.secure_enclave_create_signature(message_to_sign=signing_key_message_interop, + key=new_master_keys[0].secret)["signature"] + signing_signature_interop = self.secure_enclave_create_signature(message_to_sign=signing_key_message_interop, + key=new_signing_keys[0].secret)["signature"] + new_key_signature_interop = self.secure_enclave_create_signature(message_to_sign=signing_key_message_interop, + key=new_signing_key_interop.secret)["signature"] + + key_type = 0 + key_index = 0 + + new_public_key_interop_bytes = hex_str_to_bytes(new_public_key_interop) + signing_signature_interop_bytes = hex_str_to_bytes(signing_signature_interop) + master_signature_interop_bytes = hex_str_to_bytes(master_signature_interop) + new_key_signature_interop_bytes = hex_str_to_bytes(new_key_signature_interop) + + native_input = format_eoa(native_contract.raw_encode_call(method, + key_type, + key_index, + new_public_key_interop_bytes[:32], + new_public_key_interop_bytes[32:], + signing_signature_interop_bytes[:32], + signing_signature_interop_bytes[32:], + master_signature_interop_bytes[:32], + master_signature_interop_bytes[32:], + new_key_signature_interop_bytes[:32], + new_key_signature_interop_bytes[32:])) + + # Test before interoperability fork + try: + proxy_contract.do_call(evm_address_interop, 1, native_contract_address, 0, native_input) + fail("Interoperability call should fail before fork point") + except RuntimeError as err: + print("Expected exception thrown: {}".format(err)) + # error is raised from API since the address has no balance + assert_true("reverted" in str(err)) + + + # reach the Interoperability fork + current_best_epoch = sc_node.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + + # Estimate gas. The result will be compared with the actual used gas + exp_gas = proxy_contract.estimate_gas(evm_address_interop, 1, native_contract_address, + 0, native_input) + + # Check callTrace + trace_response = proxy_contract.do_call_trace(evm_address_interop, 1, native_contract_address, 0, + native_input) + + logging.info("trace_result for call: {}".format(trace_response)) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + tx_hash = proxy_contract.call_transaction(evm_address_interop, 1, native_contract_address, + 0, native_input) + generate_next_blocks(sc_node, "first node", 1) + self.sc_sync_all() + receipt = sc_node.rpc_eth_getTransactionReceipt(tx_hash) + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Wrong tx status in receipt") + check_key_rotation_event(receipt['result']['logs'][0], key_type, key_index, new_public_key_interop, epoch) + + # Compare estimated gas with actual used gas. They are not equal because, during the tx execution, more gas than + # actually needed is removed from the gas pool and then refunded. This causes the gas estimation algorithm to + # overestimate the gas. + + gas_used = int(receipt['result']['gasUsed'], 16) + estimated_gas = int(exp_gas['result'], 16) + assert_true(estimated_gas >= gas_used, "Wrong estimated gas") + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") + + # Check traceTransaction + trace_response = sc_node.rpc_debug_traceTransaction(tx_hash, {"tracer": "callTracer"}) + + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") if __name__ == "__main__": SCKeyRotationTest().main() diff --git a/qa/sc_evm_closed_forger.py b/qa/sc_evm_closed_forger.py index 883ef81113..6b62a940fa 100755 --- a/qa/sc_evm_closed_forger.py +++ b/qa/sc_evm_closed_forger.py @@ -30,7 +30,7 @@ Test: - Try to stake money with invalid forger info and verify that we are not allowed to stake - - Try the same with forger info pubkeys contained in the closed list, should be succesful + - Try the same with forger info pubkeys contained in the closed list, should be successful - Open the stake to the world using the openStakeTransaction and verify that a generic proposition (not included in the forger list) is allowed to forge. Some negative test is also done. diff --git a/qa/sc_evm_consensus_parameters_fork.py b/qa/sc_evm_consensus_parameters_fork.py old mode 100644 new mode 100755 diff --git a/qa/sc_evm_consensus_parameters_fork_with_mainchain_forks.py b/qa/sc_evm_consensus_parameters_fork_with_mainchain_forks.py old mode 100644 new mode 100755 diff --git a/qa/sc_evm_consensus_parameters_fork_with_sidechain_forks.py b/qa/sc_evm_consensus_parameters_fork_with_sidechain_forks.py old mode 100644 new mode 100755 diff --git a/qa/sc_evm_eoa2eoa.py b/qa/sc_evm_eoa2eoa.py index fe6e9858c4..e0d4604e87 100755 --- a/qa/sc_evm_eoa2eoa.py +++ b/qa/sc_evm_eoa2eoa.py @@ -120,6 +120,12 @@ def run_test(self): transferred_amount_in_zen) assert_true(ret, msg) + logging.info("Repeat similar transaction with different case in sender address...") + transferred_amount_in_zen = Decimal('11') + ret, msg, _ = self.makeEoa2Eoa(sc_node_1, sc_node_2, evm_address_sc1.upper(), evm_address_sc2, + transferred_amount_in_zen) + assert_true(ret, msg) + logging.info("Create an EOA to EOA transaction moving some fund from SC1 address to a SC1 different address.") evm_address_sc1_b = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] transferred_amount_in_zen = Decimal('22') @@ -163,6 +169,13 @@ def run_test(self): txJsonResult = sc_node_1.rpc_eth_getTransactionByHash(add_0x_prefix(txHash))['result'] assert_true("chainId" in txJsonResult) + logging.info("Repeat similar transaction with different case in sender address...") + transferred_amount_in_zen = Decimal('15') + ret, msg, txHash = self.makeEoa2Eoa(sc_node_1, sc_node_2, evm_address_sc1.upper(), evm_address_sc2, + transferred_amount_in_zen, + isEIP155=True, print_json_results=False) + assert_true(ret, msg) + # negative cases logging.info("Create an EOA to EOA transaction moving all the from balance") diff --git a/qa/sc_evm_forger.py b/qa/sc_evm_forger.py index 1703cd21a7..70c0e52554 100755 --- a/qa/sc_evm_forger.py +++ b/qa/sc_evm_forger.py @@ -9,15 +9,18 @@ from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup from SidechainTestFramework.account.ac_use_smart_contract import SmartContract -from SidechainTestFramework.account.ac_utils import format_eoa, format_evm, ac_makeForgerStake +from SidechainTestFramework.account.ac_utils import format_eoa, format_evm, ac_makeForgerStake, \ + generate_block_and_get_tx_receipt +from SidechainTestFramework.account.httpCalls.transaction.createEIP1559Transaction import createEIP1559Transaction from SidechainTestFramework.account.httpCalls.wallet.balance import http_wallet_balance +from SidechainTestFramework.account.simple_proxy_contract import SimpleProxyContract from SidechainTestFramework.account.utils import convertZenToWei, \ convertZenToZennies, convertZenniesToWei, computeForgedTxFee, convertWeiToZen, FORGER_STAKE_SMART_CONTRACT_ADDRESS, \ - WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS -from SidechainTestFramework.scutil import generate_next_block, SLOTS_IN_EPOCH, EVM_APP_SLOT_TIME + WITHDRAWAL_REQ_SMART_CONTRACT_ADDRESS, INTEROPERABILITY_FORK_EPOCH +from SidechainTestFramework.scutil import generate_next_block, EVM_APP_SLOT_TIME from sc_evm_test_contract_contract_deployment_and_interaction import random_byte_string from test_framework.util import ( - assert_equal, assert_true, fail, forward_transfer_to_sidechain, hex_str_to_bytes, + assert_equal, assert_true, fail, forward_transfer_to_sidechain, hex_str_to_bytes, bytes_to_hex_str, assert_false, ) """ @@ -35,8 +38,10 @@ - Check that SC2 can not forge before two epochs are passed by, and afterwards it can - SC1 spends the genesis stake - SC1 can still forge blocks but after two epochs it can not anymore + - Test the Forger Staked smart contract can be called by an EVM Smart contract - SC1 removes all remaining stakes - Verify that it is not possible to forge new SC blocks from the next epoch switch on + """ @@ -67,13 +72,14 @@ def check_make_forger_stake_event(event, source_addr, owner, amount): assert_equal(event_signature, event_id, "Wrong event signature in topics") from_addr = decode(['address'], hex_str_to_bytes(event['topics'][1][2:]))[0][2:] - assert_equal(source_addr, from_addr, "Wrong from address in topics") + assert_equal(source_addr.lower(), from_addr.lower(), "Wrong from address in topics") owner_addr = decode(['address'], hex_str_to_bytes(event['topics'][2][2:]))[0][2:] assert_equal(owner, owner_addr, "Wrong owner address in topics") (stake_id, value) = decode(['bytes32', 'uint256'], hex_str_to_bytes(event['data'][2:])) assert_equal(convertZenToWei(amount), value, "Wrong amount in event") + return stake_id def check_spend_forger_stake_event(event, owner, stake_id): @@ -93,7 +99,7 @@ def check_spend_forger_stake_event(event, owner, stake_id): class SCEvmForger(AccountChainSetup): def __init__(self): super().__init__(number_of_sidechain_nodes=2, forward_amount=100, - block_timestamp_rewind=SLOTS_IN_EPOCH * EVM_APP_SLOT_TIME * 10) + block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH) def run_test(self): @@ -466,6 +472,173 @@ def run_test(self): convertZenToWei(forgerStake2_amount), http_wallet_balance(sc_node_1, FORGER_STAKE_SMART_CONTRACT_ADDRESS), "Contract address balance is wrong.") + + ####################################################################################################### + # Interoperability test with an EVM smart contract calling forger stakes native contract + ####################################################################################################### + + # Create and deploy evm proxy contract + # Create a new sc address to be used for the interoperability tests + evm_address_interop = sc_node_1.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + new_ft_amount_in_zen = Decimal('50.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_interop, + new_ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + + generate_next_block(sc_node_1, "first node") + + # Deploy proxy contract + proxy_contract = SimpleProxyContract(sc_node_1, evm_address_interop) + + # Send some funds to the proxy smart contract. Note that nonce=1 because evm_address_interop has deployed the proxy contract. + contract_funds_in_zen = 10 + createEIP1559Transaction(sc_node_1, fromAddress=evm_address_interop, toAddress=format_eoa(proxy_contract.contract_address), + nonce=1, gasLimit=230000, maxPriorityFeePerGas=900000000, + maxFeePerGas=900000000, value=convertZenToWei(contract_funds_in_zen)) + generate_next_block(sc_node_1, "first node") + + native_contract = SmartContract("ForgerStakes") + + + # Test before interoperability fork + method = "getAllForgersStakes()" + native_input = format_eoa(native_contract.raw_encode_call(method,)) + try: + proxy_contract.do_static_call(evm_address_interop, 1, FORGER_STAKE_SMART_CONTRACT_ADDRESS, native_input) + fail("Interoperability call should fail before fork point") + except RuntimeError as err: + print("Expected exception thrown: {}".format(err)) + # error is raised from API since the address has no balance + assert_true("reverted" in str(err)) + + + # reach the Interoperability fork + current_best_epoch = sc_node_1.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node_2, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + + # Test getAllForgersStakes() + + res = proxy_contract.do_static_call(evm_address_interop, 2, FORGER_STAKE_SMART_CONTRACT_ADDRESS, native_input) + + # res is (bytes32,uint256,address, bytes32,bytes32,bytes1)[]. Its ABI encoding in this case is + # - first 32 bytes is the offset + # - second 32 bytes is array length + # - the remaining are the bytes representing the various (bytes32,uint256,bytes20, bytes32,bytes32,bytes1) tuples. + # Each tuple is formed of 192 bytes, 32 bytes for each element in the tuple. + + res = res[32:] # cut offset, don't care in this case + num_of_stakes = int(bytes_to_hex_str(res[0:32]), 16) + assert_equal(2, num_of_stakes, "wrong number of forger stakes") + res = res[32:] # cut the array length + + elem_size = 192 # 32 * 6 + list_of_elems = [res[i:i + elem_size] for i in range(0, num_of_stakes * elem_size, elem_size)] + + stake_1 = decode(['(bytes32,uint256,address,bytes32,bytes32,bytes1)'], list_of_elems[0]) + stake_2 = decode(['(bytes32,uint256,address,bytes32,bytes32,bytes1)'], list_of_elems[1]) + + # Check the stakeId + assert_equal(stakeList[0]['stakeId'], bytes_to_hex_str(stake_1[0][0]), "wrong stakeId") + assert_equal(stakeList[1]['stakeId'], bytes_to_hex_str(stake_2[0][0]), "wrong stakeId") + logging.info("stakeList: {}".format(stakeList)) + + # Test forger stake creation: delegate(bytes32,bytes32,bytes1,address) + + method = "delegate(bytes32,bytes32,bytes1,address)" + vrf_pub_key = hex_str_to_bytes(sc2_vrfPubKey) + + native_input = format_eoa(native_contract.raw_encode_call(method, hex_str_to_bytes(sc2_blockSignPubKey), + vrf_pub_key[0:32], vrf_pub_key[32:], + evm_address_interop)) + + stake_amount_in_zen = 1 + stake_amount_in_wei = convertZenToWei(stake_amount_in_zen) + + # Estimate gas. The result will be compared with the actual used gas + exp_gas = proxy_contract.estimate_gas(evm_address_interop, 2, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + stake_amount_in_wei, native_input) + + logging.info("exp_gas: {}".format(exp_gas)) + + tx_id = proxy_contract.call_transaction(evm_address_interop, 2, FORGER_STAKE_SMART_CONTRACT_ADDRESS, + stake_amount_in_wei, native_input) + receipt = generate_block_and_get_tx_receipt(sc_node_2, tx_id) + logging.info("receipt: {}".format(receipt)) + logging.info("gas used in receipt: {}".format(receipt['result']['gasUsed'])) + + # Check the status of tx + status = int(receipt['result']['status'], 16) + assert_equal(1, status, "Wrong tx status in receipt") + # Check the logs + assert_equal(1, len(receipt['result']['logs']), "Wrong number of events in receipt") + event = receipt['result']['logs'][0] + stake_id = check_make_forger_stake_event(event, proxy_contract.contract_address[2:], evm_address_interop, + stake_amount_in_zen) + + # Compare estimated gas with actual used gas. They are not equal because, during the tx execution, more gas than + # actually needed is removed from the gas pool and then refunded. This causes the gas estimation algorithm to + # overestimate the gas. + gas_used = int(receipt['result']['gasUsed'], 16) + estimated_gas = int(exp_gas['result'], 16) + assert_true(estimated_gas >= gas_used, "Wrong estimated gas") + + # Check tracer + trace_response = sc_node_1.rpc_debug_traceTransaction(tx_id, {"tracer": "callTracer"}) + logging.info(trace_response) + + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + FORGER_STAKE_SMART_CONTRACT_ADDRESS, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_equal("0x" + bytes_to_hex_str(stake_id), native_call["output"]) + assert_false("calls" in native_call) + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_true(gas_used == gas_used_tracer, "Wrong gas") + + + # remove the forger stake + # There is not an easy way to test the 'withdraw' method, so this test is skipped. The problem is that it is difficult + # to create and sign the message needed for withdrawing a stake, because I don't have a way to sign the message + # with the private key of the owner. In fact the rpc method eth_sign adds a prefix to the message that is not + # added by the Forger stake smart contract, so the message verification fails. + # The same applies to 'openStakeForgerList' method. + + # Remove the stake with API + spendForgerStakeJsonRes = sc_node_1.transaction_spendForgingStake( + json.dumps({"stakeId": bytes_to_hex_str(stake_id)})) + if "result" not in spendForgerStakeJsonRes: + fail("spend forger stake failed: " + json.dumps(spendForgerStakeJsonRes)) + else: + logging.info("Forger stake removed: " + json.dumps(spendForgerStakeJsonRes)) + self.sc_sync_all() + + # Generate SC block on SC node 2 + generate_next_block(sc_node_2, "second node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + ####################################################################################################### + # End Interoperability test + ####################################################################################################### + # SC1 remove all the remaining stakes spendForgerStakeJsonRes = sc_node_1.transaction_spendForgingStake( json.dumps({"stakeId": str(stakeId_1)})) @@ -492,7 +665,7 @@ def run_test(self): check_spend_forger_stake_event(event, evm_address_sc_node_1, stakeId_1) stakeList = sc_node_1.transaction_allForgingStakes()["result"]['stakes'] - assert_equal(len(stakeList), 1) + assert_equal(1, len(stakeList)) # Check balance account_1_balance = http_wallet_balance(sc_node_1, evm_address_sc_node_1) diff --git a/qa/sc_evm_mc_addr_ownership.py b/qa/sc_evm_mc_addr_ownership.py index 704bef2874..c495ace3e6 100755 --- a/qa/sc_evm_mc_addr_ownership.py +++ b/qa/sc_evm_mc_addr_ownership.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 import json +import logging import pprint from decimal import Decimal + from eth_abi import decode from eth_utils import add_0x_prefix, remove_0x_prefix, event_signature_to_log_topic, encode_hex, \ function_signature_to_4byte_selector, to_checksum_address + from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup -from SidechainTestFramework.account.ac_utils import format_evm, estimate_gas +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import format_evm, estimate_gas, format_eoa from SidechainTestFramework.account.httpCalls.transaction.createLegacyEIP155Transaction import \ createLegacyEIP155Transaction from SidechainTestFramework.account.httpCalls.transaction.getKeysOwnership import getKeysOwnership from SidechainTestFramework.account.httpCalls.transaction.removeKeysOwnership import removeKeysOwnership from SidechainTestFramework.account.httpCalls.transaction.sendKeysOwnership import sendKeysOwnership -from SidechainTestFramework.account.utils import MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS -from SidechainTestFramework.scutil import generate_next_block, SLOTS_IN_EPOCH, EVM_APP_SLOT_TIME +from SidechainTestFramework.account.simple_proxy_contract import SimpleProxyContract +from SidechainTestFramework.account.utils import MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS, INTEROPERABILITY_FORK_EPOCH, \ + ZENDAO_FORK_EPOCH +from SidechainTestFramework.scutil import generate_next_block, EVM_APP_SLOT_TIME from SidechainTestFramework.sidechainauthproxy import SCAPIException from httpCalls.transaction.allTransactions import allTransactions from test_framework.util import (assert_equal, assert_true, fail, hex_str_to_bytes, assert_false, @@ -29,6 +35,7 @@ - Add ownership relation and check event - Get the list of MC addresses associated to a SC address - Remove an association and check event + - Interoperability test: same tests as before but using a proxy evm smart contract Do some negative tests """ @@ -84,9 +91,9 @@ def check_receipt(sc_node, tx_hash, expected_receipt_status=1, sc_addr=None, mc_ raise Exception('Rpc eth_getTransactionReceipt cmd failed:{}'.format(json.dumps(receipt, indent=2))) status = int(receipt['result']['status'], 16) - assert_true(status == expected_receipt_status) + assert_equal(expected_receipt_status, status) - # if we have a succesful receipt and valid func parameters, check the event + # if we have a successful receipt and valid func parameters, check the event if expected_receipt_status == 1: if (sc_addr is not None) and (mc_addr is not None): assert_equal(1, len(receipt['result']['logs']), "Wrong number of events in receipt") @@ -99,18 +106,25 @@ def check_receipt(sc_node, tx_hash, expected_receipt_status=1, sc_addr=None, mc_ def check_get_key_ownership(abi_return_value, exp_dict): # the location of the data part of the first (the only one in this case) parameter (dynamic type), measured in bytes # from the start of the return data block. In this case 32 (0x20) - start_data_offset = decode(['uint32'], hex_str_to_bytes(abi_return_value[0:64]))[0] * 2 - assert_equal(start_data_offset, 64) + abi_return_value_bytes = hex_str_to_bytes(abi_return_value) + sc_associations_dict = extract_sc_associations_list(abi_return_value_bytes) + + pprint.pprint(sc_associations_dict) + res = json.dumps(sc_associations_dict) + assert_equal(res, json.dumps(dict(sorted(exp_dict.items())))) - end_offset = start_data_offset + 64 # read 32 bytes - list_size = decode(['uint32'], hex_str_to_bytes(abi_return_value[start_data_offset:end_offset]))[0] +def extract_sc_associations_list(abi_return_value_bytes): + start_data_offset = decode(['uint32'], abi_return_value_bytes[0:32])[0] + assert_equal(start_data_offset, 32) + end_offset = start_data_offset + 32 # read 32 bytes + list_size = decode(['uint32'], abi_return_value_bytes[start_data_offset:end_offset])[0] sc_associations_dict = {} for i in range(list_size): start_offset = end_offset - end_offset = start_offset + 192 # read (32 + 32 + 32) bytes + end_offset = start_offset + 96 # read (32 + 32 + 32) bytes (address_pref, mca3, mca32) = decode(['address', 'bytes3', 'bytes32'], - hex_str_to_bytes(abi_return_value[start_offset:end_offset])) + abi_return_value_bytes[start_offset:end_offset]) sc_address_checksum_fmt = to_checksum_address(address_pref) print("sc addr=" + sc_address_checksum_fmt) if sc_address_checksum_fmt in sc_associations_dict: @@ -122,20 +136,13 @@ def check_get_key_ownership(abi_return_value, exp_dict): mc_addr_list.append(mc_addr) print("mc addr=" + mc_addr) sc_associations_dict[sc_address_checksum_fmt] = mc_addr_list - - pprint.pprint(sc_associations_dict) - res = json.dumps(sc_associations_dict) - assert_equal(res, json.dumps(dict(sorted(exp_dict.items())))) - - -# The activation epoch of the zendao feature, as coded in the sdk -ZENDAO_FORK_EPOCH = 7 + return sc_associations_dict class SCEvmMcAddressOwnership(AccountChainSetup): def __init__(self): - super().__init__(number_of_sidechain_nodes=2, - block_timestamp_rewind=SLOTS_IN_EPOCH * EVM_APP_SLOT_TIME * ZENDAO_FORK_EPOCH) + super().__init__(block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH, + number_of_sidechain_nodes=2) def run_test(self): ft_amount_in_zen = Decimal('500.0') @@ -168,15 +175,16 @@ def run_test(self): assert_true(taddr1 is not None) # check the balance of native smart contract is null + native_contract_address = MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS nsc_bal = int( - sc_node.rpc_eth_getBalance(format_evm(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS), 'latest')['result'], 16) + sc_node.rpc_eth_getBalance(format_evm(native_contract_address), 'latest')['result'], 16) assert_equal(nsc_bal, 0) # send funds to native smart contract before the zendao fork is reached eoa_nsc_amount = 123456 tx_hash_eoa = createLegacyEIP155Transaction(sc_node2, fromAddress=sc_address2, - toAddress=MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS, + toAddress=native_contract_address, value=eoa_nsc_amount ) self.sc_sync_all() @@ -189,13 +197,13 @@ def run_test(self): assert_true(tx_hash_eoa not in response['transactionIds']) receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash_eoa)) status = int(receipt['result']['status'], 16) - assert_true(status == 1) + assert_equal(1, status) gas_used = int(receipt['result']['gasUsed'], 16) assert_equal(gas_used, 21000) # check the address has the expected balance nsc_bal = int( - sc_node.rpc_eth_getBalance(format_evm(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS), 'latest')['result'], 16) + sc_node.rpc_eth_getBalance(format_evm(native_contract_address), 'latest')['result'], 16) assert_equal(nsc_bal, eoa_nsc_amount) mc_signature1 = mc_node.signmessage(taddr1, sc_address_checksum_fmt) @@ -225,7 +233,7 @@ def run_test(self): gas_used = int(receipt['result']['gasUsed'], 16) assert_true(gas_used > 21000) - # reach the fork + # reach the ZenDao fork current_best_epoch = sc_node.block_forgingInfo()["result"]["bestBlockEpochNumber"] for i in range(0, ZENDAO_FORK_EPOCH - current_best_epoch): @@ -302,7 +310,7 @@ def run_test(self): assert_true(len(ret['keysOwnership']) == 0) # negative cases - # 1. try to add the an ownership already there + # 1. try to add the ownership already there try: sendKeysOwnership(sc_node, sc_address=sc_address, @@ -427,7 +435,7 @@ def run_test(self): for taddr in taddr_list: assert_true(taddr in list_associations_sc_address['keysOwnership'][sc_address_checksum_fmt]) - # remove an mc addr and check we have 11 of them + # remove a mc addr and check we have 11 of them taddr_rem = taddr_list[4] assert_true(len(taddr_list) == 10) @@ -489,10 +497,10 @@ def run_test(self): pprint.pprint(list_all_associations) # check we have two sc address associations - assert_true(len(list_all_associations['keysOwnership']) == 2) + assert_equal(2, len(list_all_associations['keysOwnership'])) # check we have the expected numbers - assert_true(len(list_all_associations['keysOwnership'][sc_address_checksum_fmt]) == 11) - assert_true(len(list_all_associations['keysOwnership'][sc_address2_checksum_fmt]) == 1) + assert_equal(11, len(list_all_associations['keysOwnership'][sc_address_checksum_fmt])) + assert_equal(1, len(list_all_associations['keysOwnership'][sc_address2_checksum_fmt])) assert_true(taddr_sc2_1 in list_all_associations['keysOwnership'][sc_address2_checksum_fmt]) # execute native smart contract for getting all associations @@ -500,7 +508,7 @@ def run_test(self): abi_str = function_signature_to_4byte_selector(method) req = { "from": format_evm(sc_address), - "to": format_evm(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS), + "to": format_evm(native_contract_address), "nonce": 3, "gasLimit": 2300000, "gasPrice": 850000000, @@ -523,7 +531,7 @@ def run_test(self): addr_padded_str = "000000000000000000000000" + sc_address req = { "from": format_evm(sc_address), - "to": format_evm(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS), + "to": format_evm(native_contract_address), "nonce": 3, "gasLimit": 2300000, "gasPrice": 850000000, @@ -558,7 +566,7 @@ def run_test(self): est_gas_nsc_data = response['transactions'][0]['data'] response = estimate_gas(sc_node2, sc_address2_checksum_fmt, - to_address=to_checksum_address(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS), + to_address=to_checksum_address(native_contract_address), data="0x" + est_gas_nsc_data, gasPrice='0x35a4e900', nonce=0) @@ -574,6 +582,237 @@ def run_test(self): gas_used = receipt['result']['gasUsed'] assert_equal(est_gas_used, gas_used) + ####################################################################################################### + # Interoperability test with an EVM smart contract calling MC address ownership native contract + ####################################################################################################### + + # Create and deploy evm proxy contract + # Create a new sc address to be used for the interoperability tests + evm_address_interop = sc_node.wallet_createPrivateKeySecp256k1()["result"]["proposition"]["address"] + + new_ft_amount_in_zen = Decimal('50.0') + + forward_transfer_to_sidechain(self.sc_nodes_bootstrap_info.sidechain_id, + mc_node, + evm_address_interop, + new_ft_amount_in_zen, + mc_return_address=mc_node.getnewaddress(), + generate_block=True) + + generate_next_block(sc_node, "first node") + + # Deploy proxy contract + proxy_contract = SimpleProxyContract(sc_node, evm_address_interop) + # Create native contract interface, useful encoding/decoding params. Note this doesn't deploy a contract. + native_contract = SmartContract("McAddrOwnership") + + # Test before interoperability fork + method = 'getAllKeyOwnerships()' + native_input = format_eoa(native_contract.raw_encode_call(method)) + try: + proxy_contract.do_static_call(evm_address_interop, 1, native_contract_address, native_input) + fail("Interoperability call should fail before fork point") + except RuntimeError as err: + print("Expected exception thrown: {}".format(err)) + # error is raised from API since the address has no balance + assert_true("reverted" in str(err)) + + + # reach the Interoperability fork + current_best_epoch = sc_node.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # Test getAllKeyOwnerships() + + res = proxy_contract.do_static_call(evm_address_interop, 1, native_contract_address, native_input) + + sc_associations_list = extract_sc_associations_list(res) + logging.info("res: {}".format(sc_associations_list)) + assert_equal(2, len(sc_associations_list), "wrong number of sidechain addresses") + assert_equal(11, len(sc_associations_list[sc_address_checksum_fmt]), + " wrong number of associations for sc_address_1") + assert_equal(2, len(sc_associations_list[sc_address2_checksum_fmt]), + "wrong number of associations for sc_address_2") + + # Test 'getKeyOwnerships(address)' + method = 'getKeyOwnerships(address)' + + native_input = format_eoa(native_contract.raw_encode_call(method, sc_address)) + + res = proxy_contract.do_static_call(evm_address_interop, 1, native_contract_address, native_input) + + sc_associations_list = extract_sc_associations_list(res) + logging.info("res: {}".format(sc_associations_list)) + assert_equal(1, len(sc_associations_list), "wrong number of sidechain addresses") + assert_equal(11, len(sc_associations_list[sc_address_checksum_fmt]), + " wrong number of associations for sc_address_1") + + # Test getKeyOwnerScAddresses() + method = 'getKeyOwnerScAddresses()' + native_input = format_eoa(native_contract.raw_encode_call(method)) + + res = proxy_contract.do_static_call(evm_address_interop, 1, native_contract_address, native_input) + + sc_address_list = native_contract.raw_decode_call_result(method, res)[0] + logging.info("sc_address_list: {}".format(sc_address_list)) + assert_equal(2, len(sc_address_list), "wrong number of sidechain addresses") + assert_true("0x" + sc_address in sc_address_list) + assert_true("0x" + sc_address2 in sc_address_list) + + # Test 'sendKeysOwnership(bytes3,bytes32,bytes24,bytes32,bytes32)' + # For this function I need a signature. Because I don't have a way to create it in this test, I'll create a + # non-executable transaction using the HTTP API and steal its data. + + taddr_interop = mc_node.getnewaddress() + sc_address_checksum_interop = to_checksum_address(evm_address_interop) + mc_signature_interop = mc_node.signmessage(taddr_interop, sc_address_checksum_interop) + + # Create a non-executable transaction with the data invoking native smart contract + ret = sendKeysOwnership(sc_node, nonce=10, + sc_address=evm_address_interop, + mc_addr=taddr_interop, + mc_signature=mc_signature_interop) + + # Get the transaction from the mempool + response = allTransactions(sc_node, True) + logging.info("response {}".format(response)) + native_input = response['transactions'][0]['data'] + + # Estimate gas. The result will be compared with the actual used gas + exp_gas = proxy_contract.estimate_gas(evm_address_interop, 2, native_contract_address, + 0, native_input) + + # Check callTrace + trace_response = proxy_contract.do_call_trace(evm_address_interop, 1, native_contract_address, 0, + native_input) + + logging.info("trace_result: {}".format(trace_response)) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + tx_hash = proxy_contract.call_transaction(evm_address_interop, 1, native_contract_address, + 0, native_input) + forge_and_check_receipt(self, sc_node, tx_hash, sc_addr=evm_address_interop, mc_addr=taddr_interop) + + # Compare estimated gas with actual used gas. They are not equal because, during the tx execution, more gas than + # actually needed is removed from the gas pool and then refunded. This causes the gas estimation algorithm to + # overestimate the gas. + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + + gas_used = int(receipt['result']['gasUsed'], 16) + estimated_gas = int(exp_gas['result'], 16) + assert_true(estimated_gas >= gas_used, "Wrong estimated gas") + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") + + # Check traceTransaction + trace_response = sc_node.rpc_debug_traceTransaction(tx_hash, {"tracer": "callTracer"}) + logging.info("rpc_debug_traceTransaction {}".format(trace_response)) + + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") + + # Test 'removeKeysOwnership(bytes3,bytes32)' + method = 'removeKeysOwnership(bytes3,bytes32)' + + taddr_interop_bytes = bytes(taddr_interop, 'utf-8') + native_input = format_eoa(native_contract.raw_encode_call(method, taddr_interop_bytes[0:3], + taddr_interop_bytes[3:])) + + # Estimate gas. The result will be compared with the actual used gas + exp_gas = proxy_contract.estimate_gas(evm_address_interop, 2, native_contract_address, + 0, native_input) + + # Check callTrace + trace_response = proxy_contract.do_call_trace(evm_address_interop, 2, native_contract_address, 0, + native_input) + + logging.info("trace_result for remove: {}".format(trace_response)) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + tx_hash = proxy_contract.call_transaction(evm_address_interop, 2, native_contract_address, + 0, native_input) + forge_and_check_receipt(self, sc_node, tx_hash, sc_addr=evm_address_interop, mc_addr=taddr_interop, evt_op="remove") + + # Compare estimated gas with actual used gas. They are not equal because, during the tx execution, more gas than + # actually needed is removed from the gas pool and then refunded. This causes the gas estimation algorithm to + # overestimate the gas. + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + + gas_used = int(receipt['result']['gasUsed'], 16) + estimated_gas = int(exp_gas['result'], 16) + assert_true(estimated_gas >= gas_used, "Wrong estimated gas") + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") + + # Check traceTransaction + trace_response = sc_node.rpc_debug_traceTransaction(tx_hash, {"tracer": "callTracer"}) + logging.info(trace_response) + + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + + assert_equal(proxy_contract.contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + native_call = trace_result["calls"][0] + assert_equal("CALL", native_call["type"]) + assert_equal(proxy_contract.contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + native_contract_address, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal("0x" + native_input, native_call["input"]) + assert_false("calls" in native_call) + + gas_used_tracer = int(trace_result['gasUsed'], 16) + assert_equal(gas_used, gas_used_tracer, "Wrong gas") + if __name__ == "__main__": SCEvmMcAddressOwnership().main() + diff --git a/qa/sc_evm_native_interop.py b/qa/sc_evm_native_interop.py new file mode 100755 index 0000000000..2b00baca44 --- /dev/null +++ b/qa/sc_evm_native_interop.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +import logging + +from eth_typing import HexStr +from eth_utils import function_signature_to_4byte_selector, encode_hex, remove_0x_prefix + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import deploy_smart_contract, format_evm +from SidechainTestFramework.account.utils import FORGER_STAKE_SMART_CONTRACT_ADDRESS, PROXY_SMART_CONTRACT_ADDRESS, \ + INTEROPERABILITY_FORK_EPOCH +from SidechainTestFramework.scutil import EVM_APP_SLOT_TIME, generate_next_block +from test_framework.util import assert_equal, assert_false, assert_true, fail + +""" +Check contracts interoperability, i.e an EVM Contract calling a native contract or vice-versa. + +Configuration: bootstrap 1 SC node and start it with genesis info extracted from a mainchain node. + - Mine some blocks to reach hard fork + - Create 1 SC node + - Extract genesis info + - Start SC node with that genesis info + +Test: + - Compile and deploy the NativeInterop contract + - Fetch all forger stakes via the NativeInterop contract + - Fetch all forger stakes by calling the native contract directly + - Verify identical results + - Compile and deploy the Storage contract + - Fetch current storage value directly via the Storage contract + - Fetch current storage value via the proxy native contract + - Verify identical results +""" + + +class SCEvmNativeInterop(AccountChainSetup): + + def __init__(self): + super().__init__(block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH, + withdrawalEpochLength=100) + + def deploy(self, contract_name, *args): + logging.info(f"Creating smart contract utilities for {contract_name}") + contract = SmartContract(contract_name) + logging.info(contract) + contract_address = deploy_smart_contract(self.sc_nodes[0], contract, self.evm_address, *args) + return contract, contract_address + + def run_test(self): + self.sc_ac_setup() + node = self.sc_nodes[0] + + """ + Tests from EVM Smart contract to Native Smart contract + """ + + # Compile and deploy the NativeInterop contract + # d9908c86: GetForgerStakes() + # 3ef7a7c9: GetForgerStakesCallCode() + # 585e290d: GetForgerStakesDelegateCall() + _, contract_address = self.deploy("NativeInterop") + + NATIVE_INTEROP_GETFORGERSTAKES_SIG = "0xd9908c86" # GetForgerStakes signature on native_interop EVM contract + FORGER_STAKE_GETFORGERSTAKES_SIG = "0xf6ad3c23" # GetForgerStakes signature on forger stake native contract + + NATIVE_INTEROP_GETFORGERSTAKES_SIG = "0xd9908c86" + + # Test before interoperability fork + actual_value = node.rpc_eth_call( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + }, "latest" + ) + assert_true("error" in actual_value) + assert_true("reverted" in actual_value["error"]["message"]) + + # reach the Interoperability fork + current_best_epoch = node.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + + + # Fetch all forger stakes via the NativeInterop contract + actual_value = node.rpc_eth_call( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + }, "latest" + ) + + # Fetch all forger stakes by calling the native contract directly + expected_value = node.rpc_eth_call( + { + "to": "0x" + FORGER_STAKE_SMART_CONTRACT_ADDRESS, + "input": FORGER_STAKE_GETFORGERSTAKES_SIG + }, "latest" + ) + + # Verify identical results + assert_equal(expected_value, actual_value, "results do not match") + + # Verify DELEGATECALL to a native contract throws an error + delegate_call_result = node.rpc_eth_call( + { + "to": contract_address, + "input": "0x585e290d" + }, "latest" + ) + assert_true("error" in delegate_call_result) + assert_true("unsupported call method" in delegate_call_result["error"]["message"]) + + # Verify CALLCODE to a native contract throws an error + call_code_result = node.rpc_eth_call( + { + "to": contract_address, + "input": "0x3ef7a7c9" + }, "latest" + ) + assert_true("error" in call_code_result) + assert_true("unsupported call method" in call_code_result["error"]["message"]) + + # Verify tracing gives reasonable result for the call from EVM contract to native contract + trace_response = node.rpc_debug_traceCall( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + }, "latest", { + "tracer": "callTracer" + } + ) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + logging.info("trace result: {}".format(trace_result)) + + + # Expected output + # { + # "type": "CALL", + # "from": "0x0000000000000000000000000000000000000000", + # "to": "0x840463d17b8c7833883eaa47d23b2646f7fd1fd9", + # "value": "0x0", + # "gas": "0x2fa9e38", + # "gasUsed": "0x6b9e", + # "input": "0xd9908c86", + # "output": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000013941b55d40a2cac0485248eca396e72237d9ca08e07f686989ffff37f1d320960000000000000000000000000000000000000000000000056bc75e2d63100000000000000000000000000000375ea7214743b3ad892beed86999a1f5a6794ad76e3bda4dfddf67e293362514c36142f70862dab22cd3609face526aec9b1c809dbfb30791dbc1b1d0140fea9c49cd2ca0d6aade8139ee919cc4795e11ae9c1040000000000000000000000000000000000000000000000000000000000000000", + # "calls": [ + # { + # "type": "STATICCALL", + # "from": "0x840463d17b8c7833883eaa47d23b2646f7fd1fd9", + # "to": "0x0000000000000000000022222222222222222222", + # "gas": "0x186a0", + # "gasUsed": "0x49d4", + # "input": "0xf6ad3c23", + # "output": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000013941b55d40a2cac0485248eca396e72237d9ca08e07f686989ffff37f1d320960000000000000000000000000000000000000000000000056bc75e2d63100000000000000000000000000000375ea7214743b3ad892beed86999a1f5a6794ad76e3bda4dfddf67e293362514c36142f70862dab22cd3609face526aec9b1c809dbfb30791dbc1b1d0140fea9c49cd2ca0d6aade8139ee919cc4795e11ae9c1040000000000000000000000000000000000000000000000000000000000000000" + # } + # ] + # } + + assert_equal(contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + assert_equal(NATIVE_INTEROP_GETFORGERSTAKES_SIG, trace_result["input"]) + native_call = trace_result["calls"][0] + assert_equal("STATICCALL", native_call["type"]) + assert_equal(contract_address.lower(), native_call["from"].lower()) + assert_equal("0x" + FORGER_STAKE_SMART_CONTRACT_ADDRESS, native_call["to"]) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal(FORGER_STAKE_GETFORGERSTAKES_SIG, native_call["input"]) + assert_true(len(native_call["output"]) > 512) + assert_false("calls" in native_call) + + # Get gas estimations + estimation_interop = node.rpc_eth_estimateGas( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + } + ) + estimation_native = node.rpc_eth_estimateGas( + { + "to": "0x" + FORGER_STAKE_SMART_CONTRACT_ADDRESS, + "input": FORGER_STAKE_GETFORGERSTAKES_SIG + } + ) + logging.info("estimated gas interop: {}".format(estimation_interop)) + logging.info("estimated gas native: {}".format(estimation_native)) + + # Gas usage given in a trace does not include intrinsic gas, we need to add it to compare with gas estimation + # 21k + (number of non-zero bytes in the input) * 16 + (number of zero bytes) * 4 + intrinsic_gas = 21000 + 4 * 16 + + # Verify gas usage reported by the trace matches with the estimated gas for a call to the EVM contract + assert_equal(int(trace_result["gasUsed"], 16), int(estimation_interop["result"], 16)) + + # Verify gas usage of the nested call to the native contract reported by the trace matches with the estimation + assert_equal(int(native_call["gasUsed"], 16) + intrinsic_gas, int(estimation_native["result"], 16)) + + # Default tracer + trace_response_1 = node.rpc_debug_traceCall( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + }, "latest" + ) + assert_false("error" in trace_response_1) + assert_true("result" in trace_response_1) + assert_equal(int(estimation_interop["result"], 16), trace_response_1["result"]["gas"]) + assert_false(trace_response_1["result"]["failed"]) + assert_equal(trace_result["output"], "0x" + trace_response_1["result"]["returnValue"]) + + # Default 4byteTracer + trace_response_1 = node.rpc_debug_traceCall( + { + "to": contract_address, + "input": NATIVE_INTEROP_GETFORGERSTAKES_SIG + }, "latest", + {"tracer": "4byteTracer"} + ) + + assert_false("error" in trace_response_1) + assert_true("result" in trace_response_1) + assert_equal(2, len(trace_response_1["result"])) + # Each element has as key SELECTOR-CALLDATASIZE and value number of occurrences of this key. + assert_equal(1, trace_response_1["result"][NATIVE_INTEROP_GETFORGERSTAKES_SIG + '-0']) + assert_equal(1, trace_response_1["result"][FORGER_STAKE_GETFORGERSTAKES_SIG + '-0']) + + """ + Tests from Native Smart contract to EVM Smart contract + """ + # Compile and deploy the Storage contract + initial_value = 142 + storage_contract, storage_contract_address = self.deploy("Storage", initial_value) + + method_inc = 'inc()' + method_retrieve = 'retrieve()' + + sol_contract_call_data_inc = storage_contract.raw_encode_call(method_inc) + sol_contract_call_data_retrieve = storage_contract.raw_encode_call(method_retrieve) + + # Get current value in Storage by calling the EVM contract directly + expected_value = node.rpc_eth_call( + { + "to": storage_contract_address, + "input": sol_contract_call_data_retrieve + }, "latest" + ) + value = int(expected_value['result'], 16) + assert_equal(initial_value, value) + + # Get current value in Storage by calling the proxy native contract + + method = 'invokeCall(address,bytes)' + abi_str = function_signature_to_4byte_selector(method) + encoded_abi_method_signature = encode_hex(abi_str) + addr_padded_str = "000000000000000000000000" + remove_0x_prefix(storage_contract_address) + data_input = encoded_abi_method_signature + addr_padded_str + data_input += "0000000000000000000000000000000000000000000000000000000000000040" + h_len = hex(len(remove_0x_prefix(sol_contract_call_data_retrieve)) // 2) + data_input += "000000000000000000000000000000000000000000000000000000000000000" + remove_0x_prefix(HexStr(h_len)) + data_input += remove_0x_prefix(sol_contract_call_data_retrieve) + data_input += "00000000000000000000000000000000000000000000000000000000" + + native_contract_address = format_evm(PROXY_SMART_CONTRACT_ADDRESS) + expected_value = node.rpc_eth_call( + { + "to": native_contract_address, + "input": data_input + }, "latest" + ) + + value = int(expected_value['result'], 16) + assert_equal(initial_value, value) + + # Verify tracing + trace_response = node.rpc_debug_traceCall( + { + "to": native_contract_address, + "input": data_input + }, "latest", { + "tracer": "callTracer" + } + ) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + logging.info("trace result: {}".format(trace_result)) + + assert_equal(native_contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + assert_equal(data_input.lower(), trace_result["input"].lower()) + evm_call = trace_result["calls"][0] + assert_equal("CALL", evm_call["type"]) + assert_equal(native_contract_address.lower(), evm_call["from"].lower()) + assert_equal(storage_contract_address.lower(), evm_call["to"].lower()) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal(sol_contract_call_data_retrieve.lower(), evm_call["input"].lower()) + output = int(evm_call["output"], 16) + assert_equal(initial_value, output) + assert_false("calls" in evm_call) + + # Get gas estimations + estimation_interop = node.rpc_eth_estimateGas( + { + "to": native_contract_address, + "input": data_input + } + ) + estimation_evm = node.rpc_eth_estimateGas( + { + "to": storage_contract_address, + "input": sol_contract_call_data_retrieve + } + ) + logging.info("estimated gas interop: {}".format(estimation_interop)) + logging.info("estimated gas evm: {}".format(estimation_evm)) + + # Gas usage given in a trace does not include intrinsic gas, we need to add it to compare with gas estimation + # 21k + (number of non-zero bytes in the input) * 16 + (number of zero bytes) * 4 + intrinsic_gas = 21000 + 4 * 16 + + # Verify gas usage reported by the trace matches with the estimated gas for a call to the EVM contract + assert_equal(int(trace_result["gasUsed"], 16), int(estimation_interop["result"], 16)) + + # Verify gas usage of the nested call to the native contract reported by the trace matches with the estimation + assert_equal(int(evm_call["gasUsed"], 16) + intrinsic_gas, int(estimation_evm["result"], 16)) + + # Default tracer + trace_response_1 = node.rpc_debug_traceCall( + { + "to": native_contract_address, + "input": data_input + }, "latest" + ) + + assert_false("error" in trace_response_1) + assert_true("result" in trace_response_1) + assert_equal(int(estimation_interop["result"], 16), trace_response_1["result"]["gas"]) + assert_false(trace_response_1["result"]["failed"]) + assert_equal(trace_result["output"], "0x" + trace_response_1["result"]["returnValue"]) + + # Default 4byteTracer + trace_response_1 = node.rpc_debug_traceCall( + { + "to": native_contract_address, + "input": data_input + }, "latest", + {"tracer": "4byteTracer"} + ) + + assert_false("error" in trace_response_1) + assert_true("result" in trace_response_1) + assert_equal(2, len(trace_response_1["result"])) + # Each element has as key SELECTOR-CALLDATASIZE and value number of occurrences of this key. + assert_equal(1, trace_response_1["result"][sol_contract_call_data_retrieve + '-0']) + assert_equal(1, trace_response_1["result"][encoded_abi_method_signature + '-128']) + + + # Verify STATICCALL to a readwrite EVM contract method throws an error + method = 'invokeStaticCall(address,bytes)' + abi_str = function_signature_to_4byte_selector(method) + encoded_abi_method_signature = encode_hex(abi_str) + addr_padded_str = "000000000000000000000000" + remove_0x_prefix(storage_contract_address) + data_input_failed = encoded_abi_method_signature + addr_padded_str + data_input_failed += "0000000000000000000000000000000000000000000000000000000000000040" + h_len = hex(len(remove_0x_prefix(sol_contract_call_data_inc)) // 2) + data_input_failed += "000000000000000000000000000000000000000000000000000000000000000" + remove_0x_prefix(HexStr(h_len)) + data_input_failed += remove_0x_prefix(sol_contract_call_data_inc) + data_input_failed += "00000000000000000000000000000000000000000000000000000000" + + result = node.rpc_eth_call( + { + "to": native_contract_address, + "input": data_input_failed + }, "latest" + ) + + assert_true('error' in result) + assert_true('write protection' in result['error']['message']) + + # Verify tracing + trace_response = node.rpc_debug_traceCall( + { + "to": native_contract_address, + "input": data_input_failed + }, "latest", { + "tracer": "callTracer" + } + ) + logging.info("trace result: {}".format(trace_response)) + assert_false("error" in trace_response) + assert_true("result" in trace_response) + trace_result = trace_response["result"] + logging.info("trace result: {}".format(trace_result)) + + assert_equal(native_contract_address.lower(), trace_result["to"].lower()) + assert_equal(1, len(trace_result["calls"])) + assert_equal(data_input_failed.lower(), trace_result["input"].lower()) + assert_equal("write protection", trace_result["error"]) + evm_call = trace_result["calls"][0] + assert_equal("STATICCALL", evm_call["type"]) + assert_equal(native_contract_address.lower(), evm_call["from"].lower()) + assert_equal(storage_contract_address.lower(), evm_call["to"].lower()) + assert_true(int(native_call["gas"], 16) > 0) + assert_true(int(native_call["gasUsed"], 16) > 0) + assert_equal(sol_contract_call_data_inc.lower(), evm_call["input"].lower()) + assert_equal("write protection", evm_call["error"]) + assert_false("calls" in evm_call) + + + +if __name__ == "__main__": + SCEvmNativeInterop().main() diff --git a/qa/sc_evm_proxy_nsc.py b/qa/sc_evm_proxy_nsc.py new file mode 100755 index 0000000000..c42b4da562 --- /dev/null +++ b/qa/sc_evm_proxy_nsc.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +import json +import logging +import pprint + +from eth_abi import decode +from eth_typing import HexStr +from eth_utils import add_0x_prefix, function_signature_to_4byte_selector, encode_hex, remove_0x_prefix + +from SidechainTestFramework.account.ac_chain_setup import AccountChainSetup +from SidechainTestFramework.account.ac_use_smart_contract import SmartContract +from SidechainTestFramework.account.ac_utils import deploy_smart_contract, ac_invokeProxy, format_evm, format_eoa +from SidechainTestFramework.account.httpCalls.transaction.createEIP1559Transaction import createEIP1559Transaction +from SidechainTestFramework.account.httpCalls.transaction.createLegacyEIP155Transaction import \ + createLegacyEIP155Transaction +from SidechainTestFramework.account.utils import PROXY_SMART_CONTRACT_ADDRESS, INTEROPERABILITY_FORK_EPOCH +from SidechainTestFramework.scutil import generate_next_blocks, generate_next_block, SLOTS_IN_EPOCH, EVM_APP_SLOT_TIME +from httpCalls.transaction.allTransactions import allTransactions +from test_framework.util import assert_equal, assert_false, assert_true, hex_str_to_bytes + +""" +Check the Proxy native contract calling an EVM contract and also itself. + +Configuration: bootstrap 1 SC node and start it with genesis info extracted from a mainchain node. + - Mine some blocks to reach hard fork + - Create 1 SC node + - Extract genesis info + - Start SC node with that genesis info + +Test: + - Test using a Proxy native smart contract for invoking a solidity smart contract + - TODO: test Proxy nsc calling another native smart contract +""" + + +def get_contract_input_data_from_mempool_tx(sc_node, tx_hash): + input_data = None + mempool_list = sc_node.transaction_allTransactions(json.dumps({"format": True}))['result']['transactions'] + for tx in mempool_list: + if tx['id'] == tx_hash: + input_data = tx['data'] + return input_data + + +NUM_OF_RECURSIONS = 10 + +# The activation epoch of the Contracts Interoperability feature, as coded in the sdk + + +class SCEvmProxyNsc(AccountChainSetup): + + def __init__(self): + super().__init__(block_timestamp_rewind=1500 * EVM_APP_SLOT_TIME * INTEROPERABILITY_FORK_EPOCH, + withdrawalEpochLength=100, max_account_slots=NUM_OF_RECURSIONS + 1, + max_nonce_gap=2 * NUM_OF_RECURSIONS + 1) + + def deploy(self, contract_name): + logging.info(f"Creating smart contract utilities for {contract_name}") + contract = SmartContract(contract_name) + logging.info(contract) + contract_address = deploy_smart_contract(self.sc_nodes[0], contract, self.evm_address) + return contract, contract_address + + def run_test(self): + self.sc_ac_setup() + sc_node = self.sc_nodes[0] + + native_contract_address = PROXY_SMART_CONTRACT_ADDRESS + + # send funds to native smart contract before the fork is reached + eoa_nsc_amount = 123456 + tx_hash_eoa = createLegacyEIP155Transaction(sc_node, + fromAddress=format_eoa(self.evm_address), + toAddress=native_contract_address, + value=eoa_nsc_amount + ) + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + # get mempool contents and check tx has been forged even if the fork is not active yet. Check the receipt + response = allTransactions(sc_node, False) + assert_true(tx_hash_eoa not in response['transactionIds']) + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash_eoa)) + status = int(receipt['result']['status'], 16) + assert_equal(1, status) + gas_used = int(receipt['result']['gasUsed'], 16) + assert_equal(gas_used, 21000) + + # check the address has the expected balance + nsc_bal = int( + sc_node.rpc_eth_getBalance(format_evm(native_contract_address), 'latest')['result'], 16) + assert_equal(nsc_bal, eoa_nsc_amount) + + # Deploy Smart Contract + smart_contract_type = 'StorageTestContract' + logging.info(f"Creating smart contract utilities for {smart_contract_type}") + smart_contract = SmartContract(smart_contract_type) + logging.info(smart_contract) + initial_message = 'Initial message' + tx_hash, smart_contract_address = smart_contract.deploy(sc_node, initial_message, + fromAddress=self.evm_address, + gasLimit=10000000, + gasPrice=900000000) + + generate_next_blocks(sc_node, "first node", 1) + self.sc_sync_all() + method_get = 'get()' + method_set = 'set(string)' + + # check we successfully deployed the smart contract, and we can get the initial string + res = smart_contract.static_call(sc_node, method_get, fromAddress=self.evm_address, + toAddress=smart_contract_address, gasPrice=900000000) + assert_equal(initial_message, res[0]) + + # Try a static call on proxy before reaching the fork point. + sol_contract_call_data_get = smart_contract.raw_encode_call(method_get) + tx_hash = ac_invokeProxy( + sc_node, + remove_0x_prefix(smart_contract_address), + sol_contract_call_data_get, + nonce=None, + static=True)['result']['transactionId'] + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + # get mempool contents and check tx has been forged even if the fork is not active yet since it is processed + # by the eoa msg processor. Check also the receipt and gas used greater than an eoa (due to contract code) + response = allTransactions(sc_node, False) + assert_true(tx_hash not in response['transactionIds']) + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + status = int(receipt['result']['status'], 16) + assert_true(1, status) + gas_used = int(receipt['result']['gasUsed'], 16) + assert_true(gas_used > 21000) + + # reach the fork + current_best_epoch = sc_node.block_forgingInfo()["result"]["bestBlockEpochNumber"] + + for i in range(0, INTEROPERABILITY_FORK_EPOCH - current_best_epoch): + generate_next_block(sc_node, "first node", force_switch_to_next_epoch=True) + self.sc_sync_all() + + # use static call proxy for getting string value from solidity smart contract + # actually this is pretty useless since we are not getting back the result, we are just checking the call is OK + # we will also test eth_call further on + sol_contract_call_data_get = smart_contract.raw_encode_call(method_get) + tx_hash_static_call_get = ac_invokeProxy( + sc_node, + remove_0x_prefix(smart_contract_address), + sol_contract_call_data_get, + nonce=None, + static=True)['result']['transactionId'] + self.sc_sync_all() + + # get contract proxy call data from tx in mempool, we will use it later + input_data_static = get_contract_input_data_from_mempool_tx(sc_node, tx_hash_static_call_get) + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + # check receipt after forging and check it is successful (read only) + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash_static_call_get)) + status = int(receipt['result']['status'], 16) + assert_true(status == 1) + + on_chain_nonce = int(sc_node.rpc_eth_getTransactionCount(format_evm(self.evm_address), 'latest')['result'], 16) + + req = { + "from": format_evm(self.evm_address), + "to": format_evm(PROXY_SMART_CONTRACT_ADDRESS), + "nonce": on_chain_nonce, + "gasLimit": 2300000, + "gasPrice": 850000000, + "value": 0, + "data": add_0x_prefix(input_data_static) + } + response = sc_node.rpc_eth_call(req, 'latest') + pprint.pprint(response) + result = remove_0x_prefix(response['result']) + start_data_offset = decode(['uint32'], hex_str_to_bytes(result[0:32 * 2]))[0] + assert_equal(start_data_offset, 32) + end_offset = start_data_offset + 32 # read 32 bytes, that is the string size + str_size = decode(['uint32'], hex_str_to_bytes(result[start_data_offset * 2:end_offset * 2]))[0] + assert_equal(initial_message, bytearray.fromhex(result[end_offset * 2:(end_offset + str_size) * 2]).decode()) + + # change the contract storage via proxy native smart contract + new_message_n1 = 'Proxy did it (n.1)' + sol_contract_call_data_set_n1 = smart_contract.raw_encode_call(method_set, new_message_n1) + + # invoke the proxy native smart contract via static call, it should fail + tx_hash_static_call_set = ac_invokeProxy( + sc_node, + remove_0x_prefix(smart_contract_address), + sol_contract_call_data_set_n1, + nonce=None, + static=True)['result']['transactionId'] + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + # check receipt after forging and check it is failed (write protection) + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash_static_call_set)) + status = int(receipt['result']['status'], 16) + assert_true(status == 0) + + # invoke the proxy native smart contract and modify data + tx_hash = ac_invokeProxy( + sc_node, + remove_0x_prefix(smart_contract_address), + sol_contract_call_data_set_n1, + nonce=None)['result']['transactionId'] + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + status = int(receipt['result']['status'], 16) + assert_true(status == 1) + + # read the new value in the solidity smart contract and check we successfully modified it + res = smart_contract.static_call(sc_node, method_get, fromAddress=self.evm_address, + toAddress=smart_contract_address, gasPrice=900000000) + assert_equal(new_message_n1, res[0]) + + # call the proxy via a hand made eth tx + + # change the contract storage via proxy native smart contract + new_message_n2 = 'Proxy did it (n.2)' + sol_contract_call_data_set_n2 = smart_contract.raw_encode_call(method_set, new_message_n2) + + method = 'invokeCall(address,bytes)' + abi_str = function_signature_to_4byte_selector(method) + encoded_abi_method_signature = encode_hex(abi_str) + addr_padded_str = "000000000000000000000000" + remove_0x_prefix(smart_contract_address) + data_input = encoded_abi_method_signature + addr_padded_str + data_input += "0000000000000000000000000000000000000000000000000000000000000040" + h_len = hex(len(remove_0x_prefix(sol_contract_call_data_set_n2)) // 2) + data_input += "00000000000000000000000000000000000000000000000000000000000000" + remove_0x_prefix(HexStr(h_len)) + data_input += remove_0x_prefix(sol_contract_call_data_set_n2) + data_input += "00000000000000000000000000000000000000000000000000000000" + + tx_hash = createEIP1559Transaction( + sc_node, fromAddress=remove_0x_prefix(self.evm_address), + toAddress=PROXY_SMART_CONTRACT_ADDRESS, + gasLimit=10000000, + maxPriorityFeePerGas=900000000, + maxFeePerGas=900000000, + value=0, + data=data_input + ) + + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + status = int(receipt['result']['status'], 16) + assert_true(status == 1) + + # read the new value in the solidity smart contract and check we succesfully modified it + res = smart_contract.static_call(sc_node, method_get, fromAddress=self.evm_address, + toAddress=smart_contract_address, gasPrice=900000000) + assert_equal(new_message_n2, res[0]) + + # call the proxy via an eth tx (recursive) + new_message_n3 = 'Proxy did it (n.3)' + sol_contract_call_data_set_n3 = smart_contract.raw_encode_call(method_set, new_message_n3) + ''' + 9b679b4d + 00000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaa + 0000000000000000000000000000000000000000000000000000000000000040 + 00000000000000000000000000000000000000000000000000000000000000e4 + 9b679b4d000000000000000000000000840463d17b8c7833883eaa47d23b2646 + f7fd1fd900000000000000000000000000000000000000000000000000000000 + 0000004000000000000000000000000000000000000000000000000000000000 + 000000644ed3885e000000000000000000000000000000000000000000000000 + 0000000000000020000000000000000000000000000000000000000000000000 + 000000000000001250726f78792064696420697420286e2e3329000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + ''' + method = 'invokeCall(address,bytes)' + abi_str = function_signature_to_4byte_selector(method) + encoded_abi_method_signature = encode_hex(abi_str) + hex_len_sol_contract_data = hex(len(remove_0x_prefix(sol_contract_call_data_set_n3)) // 2) + opcode_len = len(remove_0x_prefix(encoded_abi_method_signature)) // 2 + padding_len = 32 - opcode_len + h_len = hex(4 + (3 * 32) + int(hex_len_sol_contract_data, 16) + padding_len) + addr_padded_str1 = "000000000000000000000000" + PROXY_SMART_CONTRACT_ADDRESS + addr_padded_str2 = "000000000000000000000000" + remove_0x_prefix(smart_contract_address) + + data_input = encoded_abi_method_signature + data_input += addr_padded_str1 + data_input += "0000000000000000000000000000000000000000000000000000000000000040" + data_input += "00000000000000000000000000000000000000000000000000000000000000" + remove_0x_prefix(HexStr(h_len)) + data_input += remove_0x_prefix(encoded_abi_method_signature) + data_input += addr_padded_str2 + data_input += "0000000000000000000000000000000000000000000000000000000000000040" + data_input += "00000000000000000000000000000000000000000000000000000000000000" + remove_0x_prefix( + HexStr(hex_len_sol_contract_data)) + data_input += remove_0x_prefix(sol_contract_call_data_set_n3) + data_input += "00000000000000000000000000000000000000000000000000000000" # padding 1 + data_input += "00000000000000000000000000000000000000000000000000000000" # padding 2 + + tx_hash_rec = createEIP1559Transaction( + sc_node, fromAddress=remove_0x_prefix(self.evm_address), + toAddress=PROXY_SMART_CONTRACT_ADDRESS, + gasLimit=10000000, + maxPriorityFeePerGas=900000000, + maxFeePerGas=900000000, + value=0, + data=data_input + ) + + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash_rec)) + status = int(receipt['result']['status'], 16) + assert_true(status == 1) + + # read the new value in the solidity smart contract and check we successfully modified it + res = smart_contract.static_call(sc_node, method_get, fromAddress=self.evm_address, + toAddress=smart_contract_address, gasPrice=900000000) + assert_equal(new_message_n3, res[0]) + + # Verify tracing gives reasonable result for the call: native_contract->native_contract->EVM contract + trace_response = sc_node.rpc_debug_traceCall( + { + "to": format_evm(PROXY_SMART_CONTRACT_ADDRESS), + "nonce": 3, + "input": data_input + }, "latest", { + "tracer": "callTracer" + } + ) + pprint.pprint(trace_response) + assert_false("error" in trace_response) + + on_chain_nonce = int(sc_node.rpc_eth_getTransactionCount(format_evm(self.evm_address), 'latest')['result'], 16) + + # change the contract storage via proxy native smart contract which recursively calls itself a number of times + rec_message = 'Proxy recursively did it' + sol_contract_call_data_set_rec = smart_contract.raw_encode_call(method_set, rec_message) + + # invoke the proxy native smart contract + tx_hash = ac_invokeProxy( + sc_node, + remove_0x_prefix(smart_contract_address), + sol_contract_call_data_set_rec, + nonce=on_chain_nonce + NUM_OF_RECURSIONS)['result']['transactionId'] + self.sc_sync_all() + + # call all but the last leaving a gap in the nonce sequences, in this way they are not forged + for i in range(1, NUM_OF_RECURSIONS): + # get the tx input data from mempool + input_data = get_contract_input_data_from_mempool_tx(sc_node, tx_hash) + + # invoke the proxy native smart contract + tx_hash = ac_invokeProxy( + sc_node, + PROXY_SMART_CONTRACT_ADDRESS, + input_data, + nonce=on_chain_nonce + i + NUM_OF_RECURSIONS + )['result']['transactionId'] + self.sc_sync_all() + + # invoke the proxy native smart contract for the last time with the actual state nonce, so only this tx gets + # forged while the others remain in the mempool because of the nonce gap we built + input_data = get_contract_input_data_from_mempool_tx(sc_node, tx_hash) + tx_hash = ac_invokeProxy( + sc_node, + PROXY_SMART_CONTRACT_ADDRESS, + input_data, + nonce=on_chain_nonce + )['result']['transactionId'] + self.sc_sync_all() + + generate_next_block(sc_node, "first node") + self.sc_sync_all() + + receipt = sc_node.rpc_eth_getTransactionReceipt(add_0x_prefix(tx_hash)) + status = int(receipt['result']['status'], 16) + assert_true(status == 1) + + # Verify tracing gives reasonable result for the call native contract->native_contract->EVM contract + trace_response = sc_node.rpc_debug_traceCall( + { + "to": format_evm(PROXY_SMART_CONTRACT_ADDRESS), + "nonce": 3, + "input": add_0x_prefix(input_data) + }, "latest", { + "tracer": "callTracer" + } + ) + pprint.pprint(trace_response) + lev_k = trace_response['result'] + gas_k = lev_k['gasUsed'] + for k in range(0, NUM_OF_RECURSIONS): + # check we have no more frames than expected + assert_equal(1, len(lev_k['calls'])) + lev_k = lev_k['calls'][0] + # check each frame spends less gas than outer one + assert_true(int(gas_k, 16) > int(lev_k['gasUsed'], 16)) + gas_k = lev_k['gasUsed'] + + # check we have no more frames than expected + assert_true('calls' not in lev_k) + + # read the new value in the solidity smart contract and check we successfully modified it + res = smart_contract.static_call(sc_node, method_get, fromAddress=self.evm_address, + toAddress=smart_contract_address, gasPrice=900000000) + assert_equal(rec_message, res[0]) + + # Get gas estimations + estimation_interop = sc_node.rpc_eth_estimateGas( + { + "to": format_evm(PROXY_SMART_CONTRACT_ADDRESS), + "nonce": 3, + "input": add_0x_prefix(input_data) + } + ) + # check estimation has the same value of the outer frame in the previous trace + assert_equal(estimation_interop['result'], trace_response['result']['gasUsed']) + + +if __name__ == "__main__": + SCEvmProxyNsc().main() diff --git a/qa/sc_evm_test_debug_methods.py b/qa/sc_evm_test_debug_methods.py index e315c4c94e..5aa25a2d75 100755 --- a/qa/sc_evm_test_debug_methods.py +++ b/qa/sc_evm_test_debug_methods.py @@ -249,6 +249,31 @@ def run_test(self): res = sc_node.rpc_debug_traceCall(trace_call_args, "pending", {"tracer": "theBestTracer"}) assert_true(res['error'] is not None, "invalid tracer should fail") + # traceCall with an invalid transaction should return an error + trace_call_args['gas'] = "0x15863" # just below intrinsic gas, the tx is invalid + res = sc_node.rpc_debug_traceCall(trace_call_args, "latest", {"tracer": "callTracer"}) + + assert_true(res['error'] is not None, "invalid transaction should fail") + assert_true('intrinsic gas too low' in res['error']['message'], "wrong message") + + # traceCall with a failed transaction should return the stack trace + trace_call_args['gas'] = "0x15864" # just enough gas to cover for intrinsic gas, tx is valid but fails for OoG + res = sc_node.rpc_debug_traceCall(trace_call_args, "latest", {"tracer": "callTracer"}) + + logging.info(res) + assert_true("error" not in res, "failed transaction should not fail") + trace_result = res["result"] + + assert_true("calls" not in trace_result) + assert_equal("CREATE", trace_result["type"]) + assert_equal(0, int(trace_result["gas"], 16)) # it is the input gas without the intrinsic gas + assert_equal("0x15864", trace_result["gasUsed"]) + assert_equal(trace_call_args['input'], trace_result["input"]) + assert_true("output" not in trace_result) + assert_equal("out of gas", trace_result["error"]) + + + if __name__ == "__main__": SCEvmDebugMethods().main() diff --git a/qa/sc_node_response_along_sync.py b/qa/sc_node_response_along_sync.py index 05dc7c43df..14353fe6cd 100755 --- a/qa/sc_node_response_along_sync.py +++ b/qa/sc_node_response_along_sync.py @@ -90,15 +90,23 @@ def run_test(self): logging.info("connecting the two nodes...") connect_sc_nodes(sc_node1, 1) - logging.info("sleep 5 seconds to let it start Sync ...") - time.sleep(5) - - best2_1 = (sc_node2.block_best()['result']['block']['id']) - logging.info("Middle check") - logging.info("best 1: " + best1) - logging.info("best 2_0: " + best2_0) - logging.info("best 2_1: " + best2_1) - assert_not_equal(best2_1, best2_0, "Sync has not started") + + + attempt = 0; + synchStarted = False + while (attempt < 30 and synchStarted == False): + attempt = attempt + 1 + logging.info("sleep 1 seconds to let it start Sync ...") + time.sleep(1) + best2_1 = (sc_node2.block_best()['result']['block']['id']) + logging.info("Middle check") + logging.info("best 1: " + best1) + logging.info("best 2_0: " + best2_0) + logging.info("best 2_1: " + best2_1) + synchStarted = best2_1 != best2_0 + + if (synchStarted == False): + assert_equal(synchStarted, True, "Sync has not started after 30 seconds") assert_not_equal(best2_1, best1, " Too fast to synchronize, they are already sync") # try to add more block?? logging.info("Middle check passed") @@ -115,4 +123,4 @@ def run_test(self): if __name__ == "__main__": - SCNodeResponseAlongSync().main() + SCNodeResponseAlongSync().main() \ No newline at end of file diff --git a/qa/sc_withdrawal_certificate_after_mainchain_nodes_were_disconnected.py b/qa/sc_withdrawal_certificate_after_mainchain_nodes_were_disconnected.py new file mode 100755 index 0000000000..45128e7c8d --- /dev/null +++ b/qa/sc_withdrawal_certificate_after_mainchain_nodes_were_disconnected.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +import logging +import time + +from SidechainTestFramework.sc_boostrap_info import SCNodeConfiguration, SCCreationInfo, MCConnectionInfo, \ + SCNetworkConfiguration, KEY_ROTATION_CIRCUIT +from SidechainTestFramework.sc_test_framework import SidechainTestFramework +from SidechainTestFramework.scutil import get_withdrawal_epoch, bootstrap_sidechain_nodes, start_sc_nodes, \ + generate_next_block, \ + generate_next_blocks +from test_framework.util import assert_equal, initialize_chain_clean, start_nodes, \ + websocket_port_by_mc_node_index, connect_nodes_bi, assert_true, disconnect_nodes_bi + +""" +Configuration: + Start 2 MC nodes and 1 SC node (with default websocket configuration). + SC node connected to the first MC node. + +Test: + Connect the 2 mc nodes: first_mc_node is connected to sc_node, second_mc_node is only connected to the other mc node + Mine 10 blocks (withdrawal epoch length) + Forge a block to sync with mainchian headers, check we are at withdrawal epoch 1 + Wait for the certificate to be sent to mc mempool + Mine a mc block + Verify it's included in the next mc block + Forge 9 blocks to reach the withdrawal epoch length + Verify we're at epoch 2 + Disconnect first_mc_node from second_mc_node and vice versa + Mine 30 blocks with second_mc_node to have a 3 withdrawal epochs gap + Forge sc blocks + Verify first_mc_node is left behind + Verify sc_node is still at epoch 2 + Reconnect first_mc_node to second_mc_node + Let them sync + Verify they are at the same height + Forge a sc block and verify it contains 30 mainchain block headers + Verify sc is still at withdrawal epoch 2 + Mine another 10 mc blocks + Forge sc blocks + Verify sc update withdrawal epoch correctly: 2 (previous epoch) + 3 (epochs while first_mc_node was offline) + 1 (last epoch) = 6 + Wait for the 4 certificates to be created and sent to mc + Increase once again the withdrawal epoch + Wait for the final certificate +""" + +class WithdrawalCertificateAfterMainchainNodesWereDisconnected(SidechainTestFramework): + + number_of_mc_nodes = 2 + number_of_sidechain_nodes = 1 + sc_nodes_bootstrap_info=None + + def setup_chain(self): + initialize_chain_clean(self.options.tmpdir, self.number_of_mc_nodes) + + def setup_nodes(self): + return start_nodes(self.number_of_mc_nodes, self.options.tmpdir) + + def sc_setup_chain(self): + mc_node_1 = self.nodes[0] + sc_node_1_configuration = SCNodeConfiguration( + MCConnectionInfo(address="ws://{0}:{1}".format(mc_node_1.hostname, websocket_port_by_mc_node_index(0))) + ) + network = SCNetworkConfiguration( + SCCreationInfo(mc_node_1, 600, 10, is_non_ceasing=True, circuit_type=KEY_ROTATION_CIRCUIT, sc_creation_version=2), + sc_node_1_configuration + ) + self.sc_nodes_bootstrap_info = bootstrap_sidechain_nodes(self.options, network) + + def sc_setup_nodes(self): + return start_sc_nodes(self.number_of_sidechain_nodes, self.options.tmpdir) + + def run_test(self): + mc_nodes = self.nodes + sc_nodes = self.sc_nodes + + connect_nodes_bi(mc_nodes, 0, 1) + + logging.info("Number of started mc nodes: {0}".format(len(mc_nodes), "The number of MC nodes is not {0}.".format(self.number_of_mc_nodes))) + logging.info("Number of started sc nodes: {0}".format(len(sc_nodes), "The number of SC nodes is not {0}.".format(self.number_of_sidechain_nodes))) + + first_mainchain_node = mc_nodes[0] + second_mainchain_node = mc_nodes[1] + + sc_node = sc_nodes[0] + + ## Check node is a submitter + assert_true(sc_node.submitter_isCertificateSubmitterEnabled()["result"]["enabled"], "Node 1 submitter expected to be enabled.") + + epoch = get_withdrawal_epoch(sc_node) + assert_equal(0, epoch) + + block_hash = first_mainchain_node.generate(9)[-1] + self.sync_all() + first_mainchain_node_new_block = first_mainchain_node.getblock(block_hash) + second_mainchain_node_new_block = second_mainchain_node.getblock(block_hash) + # Check mc nodes are synced + assert_equal(first_mainchain_node_new_block, second_mainchain_node_new_block) + + generate_next_block(sc_node, "first node") + sc_block = sc_node.block_best()["result"]["block"] + # Check sc node is following the mc + assert_equal(9, len(sc_block["mainchainHeaders"])) + + # Generate one more block to reach withdrawal epoch + first_mainchain_node.generate(1) + self.sync_all() + + generate_next_block(sc_node, "first node") + + epoch = get_withdrawal_epoch(sc_node) + # Check withdrawal epoch switching + assert_equal(1, epoch) + + # Wait until Certificate will appear in MC node mempool + self.wait_for_cert_to_appear_in_mc(first_mainchain_node, sc_node) + + assert_equal(1, first_mainchain_node.getmempoolinfo()["size"], "Certificate was not added to Mc node mempool.") + sc_cert_hash = first_mainchain_node.getrawmempool()[0] + + # Generate 10 mc block and retrieve the certificate from the first one + mc_block = first_mainchain_node.generate(10)[0] + self.sync_all() + certs = first_mainchain_node.getblock(mc_block)["cert"] + assert_equal(1, len(certs)) + assert_equal(sc_cert_hash, certs[0]) + + generate_next_blocks(sc_node, "first node", 5) + + epoch = get_withdrawal_epoch(sc_node) + # Check withdrawal epoch switching + assert_equal(2, epoch) + + # Disconnect first_mainchain_node, that is connected to sc_node, from second_mainchain_node + disconnect_nodes_bi(self.nodes, 0, 1) + + # Generate 30 mc blocks to have a gap of 3 withdrawal epochs between first and second mc blocks + for _ in range(3): + second_mainchain_node.generate(9) + generate_next_block(sc_node, "first node") + + second_mainchain_node.generate(1) + generate_next_block(sc_node, "first node") + + first_mc_node_block_count = first_mainchain_node.getblockcount() + second_mc_node_block_count = second_mainchain_node.getblockcount() + + # Check the second_mainchain_node's height is 30 blocks greater than first_mainchain_node's one + assert_equal(30, second_mc_node_block_count - first_mc_node_block_count) + + # Check sc_node withdrawal epoch is still 2 + epoch = get_withdrawal_epoch(sc_node) + assert_equal(2, epoch) + + # Check the mainchain headers before reconnecting the mc nodes + generate_next_block(sc_node, "first node") + sc_block = sc_node.block_best()["result"]["block"] + # Check sc node is following the mc + assert_equal(0, len(sc_block["mainchainHeaders"])) + + # Connect the mc nodes + connect_nodes_bi(self.nodes, 0, 1) + self.sync_nodes([first_mainchain_node, second_mainchain_node]) + + first_mc_node_block_count = first_mainchain_node.getblockcount() + second_mc_node_block_count = second_mainchain_node.getblockcount() + + # Check that both mc nodes are synced again + assert_equal(first_mc_node_block_count, second_mc_node_block_count) + + # Check the sidechain has caught up with mc headers... + generate_next_block(sc_node, "first node") + sc_block = sc_node.block_best()["result"]["block"] + assert_equal(30, len(sc_block["mainchainHeaders"])) + + # ... but still at withdrawal epoch 2 + epoch = get_withdrawal_epoch(sc_node) + assert_equal(2, epoch) + + # Increase the withdrawal epoch + mc_blocks = first_mainchain_node.generate(10) + self.sync_all() + + for block in mc_blocks: + assert_equal(0, len(first_mainchain_node.getblock(block)["cert"])) + + generate_next_blocks(sc_node, "first node", 5) + # The epoch should be 6 since the sc has caught up with ma + epoch = get_withdrawal_epoch(sc_node) + assert_equal(6, epoch) + + for _ in range(epoch - 1): + generate_next_blocks(sc_node, "first node", 5) + + # Wait until Certificate will appear in MC node mempool + self.wait_for_cert_to_appear_in_mc(first_mainchain_node, sc_node) + assert_equal(1, first_mainchain_node.getmempoolinfo()["size"], "Certificate was not added to Mc node mempool.") + sc_cert_hash = first_mainchain_node.getrawmempool()[0] + + mc_block = first_mainchain_node.generate(1)[0] + self.sync_all() + certs = first_mainchain_node.getblock(mc_block)["cert"] + assert_equal(1, len(certs)) + assert_equal(sc_cert_hash, certs[0]) + + block_hash = first_mainchain_node.generate(9)[-1] + self.sync_all() + first_mainchain_node_new_block = first_mainchain_node.getblock(block_hash) + second_mainchain_node_new_block = second_mainchain_node.getblock(block_hash) + # Check mc nodes are synced + assert_equal(first_mainchain_node_new_block, second_mainchain_node_new_block) + + generate_next_block(sc_node, "first node") + sc_block = sc_node.block_best()["result"]["block"] + # Check sc node is following the mc + assert_equal(10, len(sc_block["mainchainHeaders"])) + + # Generate one more block to reach withdrawal epoch + first_mainchain_node.generate(1) + self.sync_all() + + generate_next_block(sc_node, "first node") + + epoch = get_withdrawal_epoch(sc_node) + # Check withdrawal epoch switching + assert_equal(7, epoch) + + # Wait until Certificate will appear in MC node mempool + self.wait_for_cert_to_appear_in_mc(first_mainchain_node, sc_node) + + assert_equal(1, first_mainchain_node.getmempoolinfo()["size"], "Certificate was not added to Mc node mempool.") + sc_cert_hash = first_mainchain_node.getrawmempool()[0] + + # Generate 10 mc block and retrieve the certificate from the first one + mc_block = first_mainchain_node.generate(1)[0] + self.sync_all() + certs = first_mainchain_node.getblock(mc_block)["cert"] + assert_equal(1, len(certs)) + assert_equal(sc_cert_hash, certs[0]) + + + def wait_for_cert_to_appear_in_mc(self, first_mainchain_node, sc_node, max_check_times = 100): + time.sleep(10) + check_counter = 0 + while first_mainchain_node.getmempoolinfo()["size"] == 0 and sc_node.submitter_isCertGenerationActive()["result"]["state"] \ + and check_counter < max_check_times: + print("Wait for certificate in mc mempool...") + time.sleep(2) + sc_node.block_best() # just a ping to SC node. For some reason, STF can't request SC node API after a while idle. + check_counter += 1 + + if check_counter == max_check_times: + raise Exception(f"Certificate did not appear in mc after {max_check_times} checks") + +if __name__ == "__main__": + WithdrawalCertificateAfterMainchainNodesWereDisconnected().main() \ No newline at end of file diff --git a/sdk/pom.xml b/sdk/pom.xml index f7b1e04bea..7e506299cd 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk - 0.8.1 + 0.9.0 ${project.groupId}:${project.artifactId} Zendoo is a unique sidechain and scaling solution developed by Horizen. The Zendoo ${project.artifactId} is a framework that supports the creation of sidechains and their custom business logic, with the Horizen public blockchain as the mainchain. https://github.com/${project.github.organization}/${project.artifactId} @@ -63,7 +63,7 @@ io.horizen sparkz-core_2.12 - 2.1.0 + 2.2.0 compile @@ -272,7 +272,7 @@ io.horizen libevm - 0.1.0 + 1.0.0 at.favre.lib @@ -402,6 +402,15 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + -Xss16m + + net.alchim31.maven scala-maven-plugin @@ -480,6 +489,39 @@ true + + org.jacoco + jacoco-maven-plugin + 0.8.9 + + + org/apache/logging/log4j/core/util/** + org/apache/logging/log4j/core/util/SystemClock.class + + + + + + prepare-agent + + + ../coverage-reports/${project.artifactId}-${project.version}/${project.artifactId}-${project.version}-jacoco-report.exec + true + + + + report + prepare-package + + report + + + ../coverage-reports/${project.artifactId}-${project.version}/${project.artifactId}-${project.version}-jacoco-report.exec + ../coverage-reports/${project.artifactId}-${project.version}/${project.artifactId}-${project.version}-jacoco-report + + + + diff --git a/sdk/src/main/java/io/horizen/account/abi/ABIDecoder.java b/sdk/src/main/java/io/horizen/account/abi/ABIDecoder.java index b7dc93c52a..aa3be4b1ff 100644 --- a/sdk/src/main/java/io/horizen/account/abi/ABIDecoder.java +++ b/sdk/src/main/java/io/horizen/account/abi/ABIDecoder.java @@ -2,7 +2,10 @@ import org.web3j.abi.DefaultFunctionReturnDecoder; import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; import java.util.List; @@ -10,14 +13,45 @@ public interface ABIDecoder { List> getListOfABIParamTypes(); - default int getABIDataParamsLengthInBytes(){ - return Type.MAX_BYTE_LENGTH * getListOfABIParamTypes().size(); + default int getABIDataParamsStaticLengthInBytes(){ + return Type.MAX_BYTE_LENGTH * getListOfABIParamTypes().size(); } - default T decode(byte[] abiEncodedData) { - if (abiEncodedData.length != getABIDataParamsLengthInBytes()){ - throw new IllegalArgumentException("Wrong message data field length: " + abiEncodedData.length + - ", expected: " + getABIDataParamsLengthInBytes()); + // this must be overridden by decoders who want the dynamic abiEncodedData size to be checked, for instance when + // using an Utf8String of fixed size + int DO_NOT_CHECK_DYNAMIC_SIZE = -1; + default int getABIDataParamsDynamicLengthInBytes() { return DO_NOT_CHECK_DYNAMIC_SIZE;} + + default boolean areAllArgumentsFixedLength() throws ClassNotFoundException { + List> paramsTypes = getListOfABIParamTypes(); + for (TypeReference t : paramsTypes) { + Class classType = t.getClassType(); + if (isDynamicType(classType)) { + return false; + } + } + return true; + } + + default boolean isDynamicType(Class classType) { + return DynamicArray.class.isAssignableFrom(classType) || + DynamicBytes.class.isAssignableFrom(classType) || + Utf8String.class.isAssignableFrom(classType); + } + + default T decode(byte[] abiEncodedData) throws ClassNotFoundException { + if (areAllArgumentsFixedLength()) { + if(abiEncodedData.length != getABIDataParamsStaticLengthInBytes()) { + throw new IllegalArgumentException("Wrong message data field length: " + abiEncodedData.length + + ", expected: " + getABIDataParamsStaticLengthInBytes()); + } + } else { + int dynamicLength = getABIDataParamsDynamicLengthInBytes(); + // check size of the dynamic struct if needed for this decoder + if (dynamicLength != DO_NOT_CHECK_DYNAMIC_SIZE && abiEncodedData.length != dynamicLength){ + throw new IllegalArgumentException("Wrong message data field length: " + abiEncodedData.length + + ", expected: " + dynamicLength); + } } String inputParamsString = org.web3j.utils.Numeric.toHexString(abiEncodedData); DefaultFunctionReturnDecoder decoder = new DefaultFunctionReturnDecoder(); diff --git a/sdk/src/main/java/io/horizen/account/abi/ABIEncodable.java b/sdk/src/main/java/io/horizen/account/abi/ABIEncodable.java index 641d2fd444..7921bba681 100644 --- a/sdk/src/main/java/io/horizen/account/abi/ABIEncodable.java +++ b/sdk/src/main/java/io/horizen/account/abi/ABIEncodable.java @@ -1,20 +1,19 @@ package io.horizen.account.abi; -import org.web3j.abi.DefaultFunctionEncoder; +import org.web3j.abi.TypeEncoder; import org.web3j.abi.datatypes.Type; import org.web3j.utils.Numeric; - -import java.util.Arrays; import java.util.List; public interface ABIEncodable { default byte[] encode() { - DefaultFunctionEncoder encoder = new DefaultFunctionEncoder(); List listOfABIObjs = List.of(asABIType()); - String encodedString = encoder.encodeParameters(listOfABIObjs); - return Numeric.hexStringToByteArray(encodedString); - + StringBuilder sb = new StringBuilder(); + for (Type t : listOfABIObjs) { + sb.append(TypeEncoder.encode(t)); + } + return Numeric.hexStringToByteArray(sb.toString()); } T asABIType(); diff --git a/sdk/src/main/java/io/horizen/account/state/BlockContext.java b/sdk/src/main/java/io/horizen/account/state/BlockContext.java index bdf79498be..7b82467f50 100644 --- a/sdk/src/main/java/io/horizen/account/state/BlockContext.java +++ b/sdk/src/main/java/io/horizen/account/state/BlockContext.java @@ -1,12 +1,12 @@ package io.horizen.account.state; import io.horizen.account.block.AccountBlockHeader; -import io.horizen.evm.results.EvmResult; -import io.horizen.evm.TraceOptions; import io.horizen.evm.Address; import io.horizen.evm.Hash; +import io.horizen.evm.Tracer; import java.math.BigInteger; +import java.util.Optional; public class BlockContext { public final Address forgerAddress; @@ -19,9 +19,7 @@ public class BlockContext { public final long chainID; public final HistoryBlockHashProvider blockHashProvider; public final Hash random; - private TraceOptions traceOptions; - - private EvmResult evmResult; + private Tracer tracer; public BlockContext( Address forgerAddress, @@ -67,23 +65,15 @@ public BlockContext( this.random = new Hash(blockHeader.vrfOutput().bytes()); } - public TraceOptions getTraceOptions() { - return this.traceOptions; - } - - public void enableTracer(TraceOptions options) { - this.traceOptions = options == null ? new TraceOptions() : options; - } - - public void disableTracer() { - this.traceOptions = null; + public Optional getTracer() { + return Optional.ofNullable(this.tracer); } - public EvmResult getEvmResult() { - return evmResult; + public void setTracer(Tracer tracer) { + this.tracer = tracer; } - public void setEvmResult(EvmResult evmResult) { - this.evmResult = evmResult; + public void removeTracer() { + this.tracer = null; } } diff --git a/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java b/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java index 203ba7a70a..eee012d95d 100644 --- a/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java +++ b/sdk/src/main/java/io/horizen/account/state/EvmMessageProcessor.java @@ -1,15 +1,32 @@ package io.horizen.account.state; -import io.horizen.evm.BlockHashCallback; -import io.horizen.evm.Evm; -import io.horizen.evm.EvmContext; -import io.horizen.evm.Hash; +import io.horizen.account.fork.ContractInteroperabilityFork; +import io.horizen.evm.*; +import io.horizen.evm.results.InvocationResult; import io.horizen.utils.BytesUtils; +import scala.Array; +import scala.Option; import scala.compat.java8.OptionConverters; import java.math.BigInteger; public class EvmMessageProcessor implements MessageProcessor { + private Address[] nativeContractAddresses = null; + + private Address[] getNativeContractAddresses(BaseAccountStateView view) { + if (nativeContractAddresses == null) { + nativeContractAddresses = view.getNativeSmartContractAddressList(); + } + assert nativeContractAddresses != null : "List of native smart contract addresses cannot be null"; + return nativeContractAddresses; + } + + @Override + public boolean customTracing() { + // the EVM handles all calls to the tracer, if there is one + return true; + } + @Override public void init(BaseAccountStateView view, int consensusEpochNumber) { // nothing to do here @@ -23,50 +40,67 @@ public void init(BaseAccountStateView view, int consensusEpochNumber) { * */ @Override - public boolean canProcess(Message msg, BaseAccountStateView view, int consensusEpochNumber) { - var to = msg.getTo(); + public boolean canProcess(Invocation invocation, BaseAccountStateView view, int consensusEpochNumber) { + var to = invocation.callee(); // contract deployment to a new account if (to.isEmpty()) return true; return view.isSmartContractAccount(to.get()); } @Override - public byte[] process(Message msg, BaseAccountStateView view, GasPool gas, BlockContext blockContext) + public byte[] process(Invocation invocation, BaseAccountStateView view, ExecutionContext context) throws ExecutionFailedException { // prepare context - var context = new EvmContext(); - context.chainID = BigInteger.valueOf(blockContext.chainID); - context.coinbase = blockContext.forgerAddress; - context.gasLimit = blockContext.blockGasLimit; - context.blockNumber = BigInteger.valueOf(blockContext.blockNumber); - context.time = BigInteger.valueOf(blockContext.timestamp); - context.baseFee = blockContext.baseFee; - context.random = blockContext.random; + var block = context.blockContext(); + var evmContext = new EvmContext( + BigInteger.valueOf(block.chainID), + block.forgerAddress, + block.blockGasLimit, + context.msg().getGasPrice(), + BigInteger.valueOf(block.blockNumber), + BigInteger.valueOf(block.timestamp), + block.baseFee, + block.random); // setup callback for the evm to access the block hash provider - try (var blockHashGetter = new BlockHashGetter(blockContext.blockHashProvider)) { - context.blockHashCallback = blockHashGetter; + try ( + var blockHashGetter = new BlockHashGetter(block.blockHashProvider); + var nativeContractProxy = new NativeContractProxy(context) + ) { + evmContext.setBlockHashCallback(blockHashGetter); + if (ContractInteroperabilityFork.get(block.consensusEpochNumber).active()){ + evmContext.setExternalContracts(getNativeContractAddresses(view)); + } + else { + evmContext.setExternalContracts(new Address[0]); + } + evmContext.setExternalCallback(nativeContractProxy); + evmContext.setTracer(block.getTracer().orElse(null)); + // Minus one because the depth is incremented for the call to the EvmMessageProcessor itself. + // We want to ignore that as the EVM will increment depth immediately for the first call frame. + // Basically, the depth would be incremented twice for the first EVM-based call frame without this. + evmContext.setInitialDepth(context.depth() - 1); - // execute EVM - var result = Evm.Apply( - view.getStateDbHandle(), - msg.getFrom(), - msg.getTo().orElse(null), - msg.getValue(), - msg.getData(), - // use gas from the pool not the message, because intrinsic gas was already spent at this point - gas.getGas(), - msg.getGasPrice(), - context, - blockContext.getTraceOptions() + // transform to libevm Invocation type + var evmInvocation = new io.horizen.evm.Invocation( + invocation.caller(), + invocation.callee().getOrElse(() -> null), + invocation.value(), + invocation.input(), + invocation.gasPool().getGas(), + invocation.readOnly() ); - blockContext.setEvmResult(result); + + // execute EVM + var result = Evm.Apply(view.getStateDbHandle(), evmInvocation, evmContext); + // consume gas the EVM has used: - // the EVM will never consume more gas than is available, hence this should never throw - // and ExecutionFailedException is thrown if the EVM reported "out of gas" - gas.subGas(result.usedGas); + // the EVM will never consume more gas than is available, hence consuming used gas here should never throw, + // instead the EVM will report an "out of gas" error which we throw as an ExecutionFailedException + var usedGas = invocation.gasPool().getGas().subtract(result.leftOverGas); + invocation.gasPool().subGas(usedGas); if (result.reverted) throw new ExecutionRevertedException(result.returnData); - if (!result.evmError.isEmpty()) throw new ExecutionFailedException(result.evmError); + if (!result.executionError.isEmpty()) throw new ExecutionFailedException(result.executionError); return result.returnData; } } @@ -86,4 +120,50 @@ protected Hash getBlockHash(BigInteger blockNumber) { .orElse(Hash.ZERO); } } + + private static class NativeContractProxy extends InvocationCallback { + private final ExecutionContext context; + + public NativeContractProxy(ExecutionContext context) { + this.context = context; + } + + /** + * Returns exception.toString(), but makes sure the return value is non-null and non-empty. If e.g. toString() + * is overriden in a custom exception and returns null this will return the exceptions class name instead. + */ + private String nonEmptyErrorMessage(Exception exception) { + var msg = exception.toString(); + if (msg == null || msg.isEmpty()) { + msg = exception.getClass().getName(); + } + return msg; + } + + @Override + protected InvocationResult execute(ExternalInvocation invocation) { + var gasPool = new GasPool(invocation.gas); + try { + var returnData = context.executeDepth( + // transform to SDK Invocation type + Invocation.apply( + invocation.caller, + Option.apply(invocation.callee), + invocation.value, + Option.apply(invocation.input).getOrElse(Array::emptyByteArray), + gasPool, + invocation.readOnly + ), + // advance call depth by the call depth processed by the EVM + invocation.depth - 1 + ); + return new InvocationResult(returnData, gasPool.getGas(), "", false, null); + } catch (ExecutionRevertedException e) { + // forward the revert reason if any + return new InvocationResult(e.returnData, gasPool.getGas(), nonEmptyErrorMessage(e), true, null); + } catch (Exception e) { + return new InvocationResult(new byte[0], gasPool.getGas(), nonEmptyErrorMessage(e), false, null); + } + } + } } diff --git a/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java b/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java index 2ec0572d5a..0c32c8bb38 100644 --- a/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java +++ b/sdk/src/main/java/io/horizen/account/state/MessageProcessor.java @@ -6,7 +6,7 @@ // by a specific instance of MessageProcessor. // The specific instance of MessageProcessor is selected by looping on a list (initialized // at genesis state creation) and executing the method 'canProcess'. -// Currently there are 3 main MessageProcessor types: +// Currently, there are 3 main MessageProcessor types: // - Eoa2Eoa: handling regular coin transfers between EOA accounts // - Evm: handling transactions requiring EVM invocations (such as smart contract deployment/invocation/...) // - NativeSmartContract: Handling SC custom logic not requiring EVM invocations (Forger Stake handling, Withdrawal request ...) @@ -16,27 +16,28 @@ public interface MessageProcessor { // Common pattern: declare a new native smart contract account in the View void init(BaseAccountStateView view, int consensusEpochNumber) throws MessageProcessorInitializationException; - // Checks if the processor is applicable to the Message. Some message processor can support messages when reaching + boolean customTracing(); + + // Checks if the processor is applicable to the invocation. Some message processor can support messages when reaching // a fork point, therefore we pass along the consensus epoch number, which is not stored in stateDb - boolean canProcess(Message msg, BaseAccountStateView view, int consensusEpochNumber); + boolean canProcess(Invocation invocation, BaseAccountStateView view, int consensusEpochNumber); /** - * Apply message to the given view. Possible results: + * Apply invocation to the given view. Possible results: *
    *
  • applied as expected: return byte[]
  • - *
  • message valid and (partially) executed, but operation "failed": throw ExecutionFailedException
  • - *
  • message invalid and must not exist in a block: throw any other Exception
  • + *
  • invocation valid and (partially) executed, but operation "failed": throw ExecutionFailedException
  • + *
  • invocation invalid and must not exist in a block: throw any other Exception
  • *
* - * @param msg message to apply to the state - * @param view state view - * @param gas available gas for the execution - * @param blockContext contextual information accessible during execution. It contains also the consensus epoch number + * @param invocation invocation to execute + * @param view state view + * @param context contextual information accessible during execution. It contains also the consensus epoch number * @return return data on successful execution * @throws ExecutionRevertedException revert-and-keep-gas-left, also mark the message as "failed" - * @throws ExecutionFailedException revert-and-consume-all-gas, also mark the message as "failed" - * @throws RuntimeException any other exceptions are consideres as "invalid message" + * @throws ExecutionFailedException revert-and-consume-all-gas, also mark the message as "failed" + * @throws RuntimeException any other exceptions are considered as "invalid message" */ - byte[] process(Message msg, BaseAccountStateView view, GasPool gas, BlockContext blockContext) - throws ExecutionFailedException; + byte[] process(Invocation invocation, BaseAccountStateView view, ExecutionContext context) + throws ExecutionFailedException; } diff --git a/sdk/src/main/java/io/horizen/account/state/WriteProtectionException.java b/sdk/src/main/java/io/horizen/account/state/WriteProtectionException.java new file mode 100644 index 0000000000..07f4f9ccfe --- /dev/null +++ b/sdk/src/main/java/io/horizen/account/state/WriteProtectionException.java @@ -0,0 +1,7 @@ +package io.horizen.account.state; + +public class WriteProtectionException extends ExecutionFailedException { + public WriteProtectionException(String message) { + super(message); + } +} diff --git a/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala b/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala index b7b92ff65d..0f361add70 100644 --- a/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala +++ b/sdk/src/main/scala/io/horizen/AbstractSidechainApp.scala @@ -137,7 +137,6 @@ abstract class AbstractSidechainApp val consensusParamsForkList = forkConfigurator.getOptionalSidechainForks.asScala.filter(fork => fork.getValue.isInstanceOf[ConsensusParamsFork]) val defaultConsensusForks: ConsensusParamsFork = ConsensusParamsFork.DefaultConsensusParamsFork - val maxConsensusSlotsInEpoch = ConsensusParamsFork.getMaxPossibleSlotsEver(consensusParamsForkList.asJava) // Init proper NetworkParams depend on MC network lazy val params: NetworkParams = sidechainSettings.genesisData.mcNetwork match { diff --git a/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala b/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala index 3635a7c5ee..5a710b552e 100644 --- a/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala +++ b/sdk/src/main/scala/io/horizen/AbstractSidechainNodeViewHolder.scala @@ -84,7 +84,7 @@ abstract class AbstractSidechainNodeViewHolder[ } catch { case e: Exception => // can happen during unit test with mocked objects - log.warn("Could not print debug info about storages: " + e.getMessage) + log.warn("Could not print debug info about storages: " + e.getMessage, e) } } diff --git a/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala b/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala index 400f379322..9ba9bb4651 100644 --- a/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala +++ b/sdk/src/main/scala/io/horizen/account/AccountSidechainApp.scala @@ -4,8 +4,8 @@ import akka.actor.ActorRef import com.google.inject.Inject import com.google.inject.name.Named import io.horizen._ -import io.horizen.account.api.http.{AccountApplicationApiGroup, route} import io.horizen.account.api.http.route.{AccountApplicationApiRoute, AccountBlockApiRoute, AccountTransactionApiRoute, AccountWalletApiRoute} +import io.horizen.account.api.http.{AccountApplicationApiGroup, route} import io.horizen.account.api.rpc.handler.RpcHandler import io.horizen.account.api.rpc.service.{EthService, RpcProcessor, RpcUtils} import io.horizen.account.block.{AccountBlock, AccountBlockHeader, AccountBlockSerializer} @@ -23,9 +23,9 @@ import io.horizen.api.http._ import io.horizen.api.http.route.{MainchainBlockApiRoute, SidechainNodeApiRoute, SidechainSubmitterApiRoute} import io.horizen.block.SidechainBlockBase import io.horizen.certificatesubmitter.network.CertificateSignaturesManagerRef -import io.horizen.consensus.{ConsensusDataStorage, ConsensusParamsUtil} +import io.horizen.consensus.ConsensusDataStorage import io.horizen.evm.LevelDBDatabase -import io.horizen.fork.{ConsensusParamsFork, ForkConfigurator} +import io.horizen.fork.ForkConfigurator import io.horizen.helper.{NodeViewProvider, NodeViewProviderImpl, TransactionSubmitProvider, TransactionSubmitProviderImpl} import io.horizen.network.SyncStatusActorRef import io.horizen.node.NodeWalletBase @@ -33,7 +33,7 @@ import io.horizen.secret.SecretSerializer import io.horizen.storage._ import io.horizen.storage.leveldb.VersionedLevelDbStorageAdapter import io.horizen.transaction._ -import io.horizen.utils.{BytesUtils, Pair, TimeToEpochUtils} +import io.horizen.utils.{BytesUtils, Pair} import sparkz.core.api.http.ApiRoute import sparkz.core.serialization.SparkzSerializer import sparkz.core.transaction.Transaction @@ -93,21 +93,21 @@ class AccountSidechainApp @Inject() // Init all storages protected val sidechainHistoryStorage = new AccountHistoryStorage( - registerClosableResource(new VersionedLevelDbStorageAdapter(historyStore, maxConsensusSlotsInEpoch * 2 + 1)), + registerClosableResource(new VersionedLevelDbStorageAdapter(historyStore, 5)), sidechainTransactionsCompanion, params) protected val sidechainSecretStorage = new SidechainSecretStorage( - registerClosableResource(new VersionedLevelDbStorageAdapter(secretStore, maxConsensusSlotsInEpoch * 2 + 1)), + registerClosableResource(new VersionedLevelDbStorageAdapter(secretStore, 5)), sidechainSecretsCompanion) protected val stateMetadataStorage = new AccountStateMetadataStorage( - registerClosableResource(new VersionedLevelDbStorageAdapter(metaStateStore, maxConsensusSlotsInEpoch * 2 + 1))) + registerClosableResource(new VersionedLevelDbStorageAdapter(metaStateStore, params.maxHistoryRewritingLength * 2))) protected val stateDbStorage: LevelDBDatabase = registerClosableResource(new LevelDBDatabase(dataDirAbsolutePath + "/evm-state")) protected val consensusDataStorage = new ConsensusDataStorage( - registerClosableResource(new VersionedLevelDbStorageAdapter(consensusStore, maxConsensusSlotsInEpoch * 2 + 1))) + registerClosableResource(new VersionedLevelDbStorageAdapter(consensusStore, 5))) // Append genesis secrets if we start the node first time if(sidechainSecretStorage.isEmpty) { diff --git a/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala b/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala index 458f24bca3..5e0bcd09a4 100644 --- a/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala +++ b/sdk/src/main/scala/io/horizen/account/AccountSidechainNodeViewHolder.scala @@ -77,9 +77,11 @@ class AccountSidechainNodeViewHolder(sidechainSettings: SidechainSettings, log.debug(s"history bestBlockId = $historyVersion, stateVersion = $checkedStateVersion") - val height_h = restoredHistory.blockInfoById(restoredHistory.bestBlockId).height - val height_s = restoredHistory.blockInfoById(checkedStateVersion).height - log.debug(s"history height = $height_h, state height = $height_s") + log.whenDebugEnabled { + val height_h = restoredHistory.blockInfoById(restoredHistory.bestBlockId).height + val height_s = restoredHistory.blockInfoById(checkedStateVersion).height + s"history height = $height_h, state height = $height_s" + } if (historyVersion == checkedStateVersion) { log.info("state and history storages are consistent") @@ -167,7 +169,7 @@ class AccountSidechainNodeViewHolder(sidechainSettings: SidechainSettings, override protected def getNodeView(): AccountNodeView = new AccountNodeView(history(), minimalState(), vault(), memoryPool()) - override val listOfStorageInfo: Seq[SidechainStorageInfo] = Seq[SidechainStorageInfo]( + override lazy val listOfStorageInfo: Seq[SidechainStorageInfo] = Seq[SidechainStorageInfo]( historyStorage, consensusDataStorage, stateMetadataStorage, secretStorage) override protected def applyLocallyGeneratedTransactions(newTxs: Iterable[SidechainTypes#SCAT]): Unit = { diff --git a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountTransactionApiRoute.scala b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountTransactionApiRoute.scala index 0de5f64a16..807c755de4 100644 --- a/sdk/src/main/scala/io/horizen/account/api/http/route/AccountTransactionApiRoute.scala +++ b/sdk/src/main/scala/io/horizen/account/api/http/route/AccountTransactionApiRoute.scala @@ -18,7 +18,7 @@ import io.horizen.account.secret.PrivateKeySecp256k1 import io.horizen.account.state.McAddrOwnershipMsgProcessor.{getMcSignature, getOwnershipId} import io.horizen.account.state._ import io.horizen.account.transaction.EthereumTransaction -import io.horizen.account.utils.WellKnownAddresses.{FORGER_STAKE_SMART_CONTRACT_ADDRESS, MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS} +import io.horizen.account.utils.WellKnownAddresses.{FORGER_STAKE_SMART_CONTRACT_ADDRESS, MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS, PROXY_SMART_CONTRACT_ADDRESS} import io.horizen.account.utils.{EthereumTransactionUtils, ZenWeiConverter} import io.horizen.api.http.JacksonSupport._ import io.horizen.api.http.route.TransactionBaseErrorResponse.{ErrorBadCircuit, ErrorByteTransactionParsing} @@ -31,7 +31,7 @@ import io.horizen.cryptolibprovider.CryptoLibProvider import io.horizen.evm.Address import io.horizen.json.Views import io.horizen.node.NodeWalletBase -import io.horizen.params.NetworkParams +import io.horizen.params.{NetworkParams, RegTestParams} import io.horizen.proof.{SchnorrSignatureSerializer, Signature25519} import io.horizen.proposition.{MCPublicKeyHashPropositionSerializer, PublicKey25519Proposition, SchnorrPropositionSerializer, VrfPublicKey} import io.horizen.secret.PrivateKey25519 @@ -71,7 +71,7 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, allTransactions ~ createLegacyEIP155Transaction ~ createEIP1559Transaction ~ createLegacyTransaction ~ sendTransaction ~ signTransaction ~ makeForgerStake ~ withdrawCoins ~ spendForgingStake ~ createSmartContract ~ allWithdrawalRequests ~ allForgingStakes ~ myForgingStakes ~ decodeTransactionBytes ~ openForgerList ~ allowedForgerList ~ createKeyRotationTransaction ~ - sendKeysOwnership ~ getKeysOwnership ~ removeKeysOwnership ~ getKeysOwnerScAddresses + invokeProxyCall ~ invokeProxyStaticCall ~ sendKeysOwnership ~ getKeysOwnership ~ removeKeysOwnership ~ getKeysOwnerScAddresses } private def getFittingSecret(nodeView: AccountNodeView, fromAddress: Option[String], txValueInWei: BigInteger) @@ -82,7 +82,7 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, val secret = allAccounts.find( a => (fromAddress.isEmpty || - BytesUtils.toHexString(a.asInstanceOf[PrivateKeySecp256k1].publicImage.address.toBytes) == fromAddress.get) && + BytesUtils.toHexString(a.asInstanceOf[PrivateKeySecp256k1].publicImage.address.toBytes).equalsIgnoreCase(fromAddress.get)) && nodeView.getNodeState.getBalance(a.asInstanceOf[PrivateKeySecp256k1].publicImage.address) .compareTo(txValueInWei) >= 0 ) @@ -900,6 +900,110 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, } + def invokeProxyCall: Route = (post & path("invokeProxyCall")) { + withBasicAuth { + _ => { + entity(as[ReqInvokeProxyCall]) { body => + // lock the view and try to create CoreTransaction + applyOnNodeView { sidechainNodeView => + val valueInWei = BigInteger.ZERO + + // default gas related params + val baseFee = sidechainNodeView.getNodeState.getNextBaseFee + var maxPriorityFeePerGas = BigInteger.valueOf(120) + var maxFeePerGas = BigInteger.TWO.multiply(baseFee).add(maxPriorityFeePerGas) + var gasLimit = BigInteger.valueOf(500000) + + if (body.gasInfo.isDefined) { + maxFeePerGas = body.gasInfo.get.maxFeePerGas + maxPriorityFeePerGas = body.gasInfo.get.maxPriorityFeePerGas + gasLimit = body.gasInfo.get.gasLimit + } + + val txCost = valueInWei.add(maxFeePerGas.multiply(gasLimit)) + + val secret = getFittingSecret(sidechainNodeView, None, txCost) + + secret match { + case Some(secret) => + + val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(secret.publicImage.address)) + val dataBytes = encodeInvokeProxyCallCmdRequest(body.invokeInfo) + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(PROXY_SMART_CONTRACT_ADDRESS)), + nonce, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + valueInWei, + dataBytes, + null + ) + validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) + case None => + ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) + } + } + } + } + } + } + + + + def invokeProxyStaticCall: Route = (post & path("invokeProxyStaticCall")) { + withBasicAuth { + _ => { + entity(as[ReqInvokeProxyCall]) { body => + // lock the view and try to create CoreTransaction + applyOnNodeView { sidechainNodeView => + val valueInWei = ZenWeiConverter.convertZenniesToWei(0) + + // default gas related params + val baseFee = sidechainNodeView.getNodeState.getNextBaseFee + var maxPriorityFeePerGas = BigInteger.valueOf(120) + var maxFeePerGas = BigInteger.TWO.multiply(baseFee).add(maxPriorityFeePerGas) + var gasLimit = BigInteger.valueOf(500000) + + if (body.gasInfo.isDefined) { + maxFeePerGas = body.gasInfo.get.maxFeePerGas + maxPriorityFeePerGas = body.gasInfo.get.maxPriorityFeePerGas + gasLimit = body.gasInfo.get.gasLimit + } + + val txCost = valueInWei.add(maxFeePerGas.multiply(gasLimit)) + + val secret = getFittingSecret(sidechainNodeView, None, txCost) + + secret match { + case Some(secret) => + + val nonce = body.nonce.getOrElse(sidechainNodeView.getNodeState.getNonce(secret.publicImage.address)) + val dataBytes = encodeInvokeProxyStaticCallCmdRequest(body.invokeInfo) + val tmpTx: EthereumTransaction = new EthereumTransaction( + params.chainId, + JOptional.of(new AddressProposition(PROXY_SMART_CONTRACT_ADDRESS)), + nonce, + gasLimit, + maxPriorityFeePerGas, + maxFeePerGas, + valueInWei, + dataBytes, + null + ) + validateAndSendTransaction(signTransactionWithSecret(secret, tmpTx)) + case None => + ApiResponseUtil.toResponse(ErrorInsufficientBalance("No account with enough balance found", JOptional.empty())) + } + } + } + } + } + } + + + def encodeAddNewStakeCmdRequest(forgerStakeInfo: TransactionForgerOutput): Array[Byte] = { val blockSignPublicKey = new PublicKey25519Proposition(BytesUtils.fromHexString(forgerStakeInfo.blockSignPublicKey.getOrElse(forgerStakeInfo.ownerAddress))) val vrfPubKey = new VrfPublicKey(BytesUtils.fromHexString(forgerStakeInfo.vrfPubKey)) @@ -908,6 +1012,18 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, Bytes.concat(BytesUtils.fromHexString(ForgerStakeMsgProcessor.AddNewStakeCmd), addForgerStakeInput.encode()) } + def encodeInvokeProxyStaticCallCmdRequest(invokeInfo: TransactionInvokeProxyCall): Array[Byte] = { + val invokeInput = InvokeSmartContractCmdInput(new Address("0x" + invokeInfo.contractAddress), invokeInfo.dataStr) + + Bytes.concat(BytesUtils.fromHexString(ProxyMsgProcessor.InvokeSmartContractStaticCallCmd), invokeInput.encode()) + } + + def encodeInvokeProxyCallCmdRequest(invokeInfo: TransactionInvokeProxyCall): Array[Byte] = { + val invokeInput = InvokeSmartContractCmdInput(new Address("0x" + invokeInfo.contractAddress), invokeInfo.dataStr) + + Bytes.concat(BytesUtils.fromHexString(ProxyMsgProcessor.InvokeSmartContractCallCmd), invokeInput.encode()) + } + def encodeOpenStakeCmdRequest(forgerIndex: Int, signature: Signature25519): Array[Byte] = { val openStakeForgerListInput = OpenStakeForgerListCmdInput(forgerIndex, signature) Bytes.concat(BytesUtils.fromHexString(ForgerStakeMsgProcessor.OpenStakeForgerListCmd), @@ -985,6 +1101,17 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, } override def listOfDisabledEndpoints(params: NetworkParams): Seq[(EndpointPrefix, EndpointPath, Option[ErrorMsg])] = { + + val proxyRoutes = params match { + case _: RegTestParams => Seq.empty + case _ => + val error = Some("This operation is enabled only on RegTest network") + Seq( + (transactionPathPrefix, "invokeProxyCall", error), + (transactionPathPrefix, "invokeProxyStaticCall", error), + ) + } + if (!params.isHandlingTransactionsEnabled) { val error = Some(ErrorNotEnabledOnSeederNode.description) Seq( @@ -999,9 +1126,9 @@ case class AccountTransactionApiRoute(override val settings: RESTApiSettings, (transactionPathPrefix, "createSmartContract", error), (transactionPathPrefix, "openForgerList", error), (transactionPathPrefix, "createKeyRotationTransaction", error), - ) + ) ++ proxyRoutes } else - Seq.empty + proxyRoutes } } @@ -1040,6 +1167,9 @@ object AccountTransactionRestScheme { @JsonView(Array(classOf[Views.Default])) private[horizen] case class TransactionRemoveMcAddrOwnershipInfo(var scAddress: String, mcTransparentAddress: Option[String]) + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class TransactionInvokeProxyCall(contractAddress: String, dataStr: String) + @JsonView(Array(classOf[Views.Default])) private[horizen] case class EIP1559GasInfo(gasLimit: BigInteger, maxPriorityFeePerGas: BigInteger, maxFeePerGas: BigInteger) { require(gasLimit.signum() > 0, "Gas limit can not be 0") @@ -1119,6 +1249,15 @@ object AccountTransactionRestScheme { @JsonView(Array(classOf[Views.Default])) private[horizen] case class ReqGetOwnerScAddresses() {} + + @JsonView(Array(classOf[Views.Default])) + private[horizen] case class ReqInvokeProxyCall( + nonce: Option[BigInteger], + invokeInfo: TransactionInvokeProxyCall, + gasInfo: Option[EIP1559GasInfo] + ) { + } + @JsonView(Array(classOf[Views.Default])) private[horizen] case class ReqOpenStakeForgerList( nonce: Option[BigInteger], diff --git a/sdk/src/main/scala/io/horizen/account/api/rpc/service/EthService.scala b/sdk/src/main/scala/io/horizen/account/api/rpc/service/EthService.scala index ce3c91e89a..8bd917484f 100644 --- a/sdk/src/main/scala/io/horizen/account/api/rpc/service/EthService.scala +++ b/sdk/src/main/scala/io/horizen/account/api/rpc/service/EthService.scala @@ -28,7 +28,7 @@ import io.horizen.account.wallet.AccountWallet import io.horizen.api.http.SidechainTransactionActor.ReceivableMessages.BroadcastTransaction import io.horizen.chain.SidechainBlockInfo import io.horizen.evm.results.ProofAccountResult -import io.horizen.evm.{Address, Hash, TraceOptions} +import io.horizen.evm.{Address, Hash, TraceOptions, Tracer} import io.horizen.forge.MainchainSynchronizer import io.horizen.network.SyncStatus import io.horizen.network.SyncStatusActor.ReceivableMessages.GetSyncStatus @@ -860,9 +860,6 @@ class EthService( // get state at previous block getStateViewAtTag(nodeView, (blockInfo.height - 1).toString) { (tagStateView, blockContext) => - // enable tracing - blockContext.enableTracer(config) - // apply mainchain references for (mcBlockRefData <- block.mainchainBlockReferencesData) { tagStateView.applyMainchainBlockReferenceData(mcBlockRefData) @@ -871,18 +868,16 @@ class EthService( val gasPool = new GasPool(block.header.gasLimit) // apply all transaction, collecting traces on the way - val evmResults = block.transactions.zipWithIndex.map({ case (tx, i) => - tagStateView.applyTransaction(tx, i, gasPool, blockContext) - blockContext.getEvmResult + val traces = block.transactions.zipWithIndex.map({ case (tx, i) => + using(new Tracer(config)) { tracer => + blockContext.setTracer(tracer) + tagStateView.applyTransaction(tx, i, gasPool, blockContext) + tracer.getResult.result + } }) // return the list of tracer results from the evm - val tracerResultList = new ListBuffer[JsonNode] - for (evmResult <- evmResults) { - if (evmResult != null && evmResult.tracerResult != null) - tracerResultList += evmResult.tracerResult - } - tracerResultList.toList + traces.toList } } @@ -940,18 +935,13 @@ class EthService( tagStateView.applyTransaction(tx, i, gasPool, blockContext) } - // enable tracing - blockContext.enableTracer(config) - - // apply requested transaction with tracing enabled - tagStateView.applyTransaction(requestedTx, previousTransactions.length, gasPool, blockContext) - - // return the tracer result from the evm - if (blockContext.getEvmResult != null && blockContext.getEvmResult.tracerResult != null) - blockContext.getEvmResult.tracerResult - else { - logger.warn("Unable to get tracer result from EVM") - JsonNodeFactory.instance.objectNode() + using(new Tracer(config)) { tracer => + // enable tracing + blockContext.setTracer(tracer) + // apply requested transaction with tracing enabled + tagStateView.applyTransaction(requestedTx, previousTransactions.length, gasPool, blockContext) + // return the tracer result + tracer.getResult.result } } } @@ -960,7 +950,6 @@ class EthService( @RpcMethod("debug_traceCall") @RpcOptionalParameters(1) def traceCall(params: TransactionArgs, tag: String, config: TraceOptions): JsonNode = { - applyOnAccountView { nodeView => // get block info val blockInfo = getBlockInfoById(nodeView, getBlockIdByHashOrTag(nodeView, tag)) @@ -968,19 +957,15 @@ class EthService( // get state at selected block getStateViewAtTag(nodeView, if (tag == "pending") "pending" else blockInfo.height.toString) { (tagStateView, blockContext) => - // enable tracing - blockContext.enableTracer(config) - - // apply requested message with tracing enabled - val msg = params.toMessage(blockContext.baseFee, settings.globalRpcGasCap) - tagStateView.applyMessage(msg, new GasPool(msg.getGasLimit), blockContext) - - // return the tracer result from the evm - if (blockContext.getEvmResult != null && blockContext.getEvmResult.tracerResult != null) - blockContext.getEvmResult.tracerResult - else { - logger.warn("Unable to get tracer result from EVM") - JsonNodeFactory.instance.objectNode() + using(new Tracer(config)) { tracer => + // enable tracing + blockContext.setTracer(tracer) + // apply requested message with tracing enabled + val msg = params.toMessage(blockContext.baseFee, settings.globalRpcGasCap) + Try(tagStateView.applyMessage(msg, new GasPool(msg.getGasLimit), blockContext)) match { + case Failure(ex) if !ex.isInstanceOf[ExecutionFailedException] => throw ex + case _ => tracer.getResult.result // return the tracer result + } } } } diff --git a/sdk/src/main/scala/io/horizen/account/fork/ContractInteroperabilityFork.scala b/sdk/src/main/scala/io/horizen/account/fork/ContractInteroperabilityFork.scala new file mode 100644 index 0000000000..7d04215254 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/fork/ContractInteroperabilityFork.scala @@ -0,0 +1,13 @@ +package io.horizen.account.fork + +import io.horizen.fork.{ForkManager, OptionalSidechainFork} + +case class ContractInteroperabilityFork(active: Boolean = false) extends OptionalSidechainFork + +object ContractInteroperabilityFork { + def get(epochNumber: Int): ContractInteroperabilityFork = { + ForkManager.getOptionalSidechainFork[ContractInteroperabilityFork](epochNumber).getOrElse(DefaultFork) + } + + private val DefaultFork: ContractInteroperabilityFork = ContractInteroperabilityFork() +} \ No newline at end of file diff --git a/sdk/src/main/scala/io/horizen/account/state/AccountState.scala b/sdk/src/main/scala/io/horizen/account/state/AccountState.scala index 5b3dd713a0..3eff92982d 100644 --- a/sdk/src/main/scala/io/horizen/account/state/AccountState.scala +++ b/sdk/src/main/scala/io/horizen/account/state/AccountState.scala @@ -316,7 +316,6 @@ class AccountState( override def isSwitchingConsensusEpoch(blockTimeStamp: Long): Boolean = { val blockConsensusEpoch: ConsensusEpochNumber = TimeToEpochUtils.timeStampToEpochNumber(params.sidechainGenesisBlockTimestamp, blockTimeStamp) val currentConsensusEpoch: ConsensusEpochNumber = getConsensusEpochNumber.getOrElse(intToConsensusEpochNumber(0)) - blockConsensusEpoch != currentConsensusEpoch } diff --git a/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala b/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala index 2105cc6589..3d716d77cc 100644 --- a/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala +++ b/sdk/src/main/scala/io/horizen/account/state/AccountStateView.scala @@ -25,7 +25,6 @@ class AccountStateView( messageProcessors: Seq[MessageProcessor] ) extends StateDbAccountStateView(stateDb, messageProcessors) with StateView[SidechainTypes#SCAT] - with AutoCloseable with SparkzLogging { def addTopQualityCertificates(refData: MainchainBlockReferenceData, blockId: ModifierId): Unit = { diff --git a/sdk/src/main/scala/io/horizen/account/state/BaseAccountStateView.scala b/sdk/src/main/scala/io/horizen/account/state/BaseAccountStateView.scala index f7f971a470..fea96e683d 100644 --- a/sdk/src/main/scala/io/horizen/account/state/BaseAccountStateView.scala +++ b/sdk/src/main/scala/io/horizen/account/state/BaseAccountStateView.scala @@ -23,4 +23,6 @@ trait BaseAccountStateView extends AccountStateReader { def addLog(log: EthereumConsensusDataLog): Unit def getGasTrackedView(gas: GasPool): BaseAccountStateView + + def getNativeSmartContractAddressList(): Array[Address] } diff --git a/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala index 10cabb074e..cb25c72d18 100644 --- a/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessor.scala @@ -37,11 +37,11 @@ case class CertificateKeyRotationMsgProcessor(params: NetworkParams) extends Nat override val contractCode: Array[Byte] = CertificateKeyRotationContractCode @throws(classOf[ExecutionFailedException]) - override def process(msg: Message, view: BaseAccountStateView, gas: GasPool, blockContext: BlockContext): Array[Byte] = { - val gasView = view.getGasTrackedView(gas) - getFunctionSignature(msg.getData) match { + override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + val gasView = view.getGasTrackedView(invocation.gasPool) + getFunctionSignature(invocation.input) match { case SubmitKeyRotationReqCmdSig => - execSubmitKeyRotation(msg, gasView, blockContext.withdrawalEpochNumber) + execSubmitKeyRotation(invocation, gasView, context.blockContext.withdrawalEpochNumber) case functionSig => throw new ExecutionRevertedException(s"Requested function does not exist. Function signature: $functionSig") @@ -145,11 +145,11 @@ case class CertificateKeyRotationMsgProcessor(params: NetworkParams) extends Nat throw new ExecutionRevertedException(s"Key rotation proof - self signature is invalid: $index") } - private def execSubmitKeyRotation(msg: Message, view: BaseAccountStateView, currentEpochNum: Int): Array[Byte] = { + private def execSubmitKeyRotation(invocation: Invocation, view: BaseAccountStateView, currentEpochNum: Int): Array[Byte] = { //verify - checkMessageValidity(msg) + checkInvocationValidity(invocation) - val inputData = SubmitKeyRotationCmdInputDecoder.decode(getArgumentsFromData(msg.getData)) + val inputData = SubmitKeyRotationCmdInputDecoder.decode(getArgumentsFromData(invocation.input)) val keyRotationProof = inputData.keyRotationProof val keyIndex = keyRotationProof.index val keyType = keyRotationProof.keyType @@ -182,13 +182,11 @@ case class CertificateKeyRotationMsgProcessor(params: NetworkParams) extends Nat keyRotationProof.encode() } - private def checkMessageValidity(msg: Message): Unit = { - val msgValue = msg.getValue - - if (msg.getData.length != METHOD_ID_LENGTH + SubmitKeyRotationCmdInputDecoder.getABIDataParamsLengthInBytes) { - throw new ExecutionRevertedException(s"Wrong message data field length: ${msg.getData.length}") - } else if (msgValue.signum() != 0) { - throw new ExecutionRevertedException(s"SubmitKeyRotation message value is non-zero: $msg") + private def checkInvocationValidity(invocation: Invocation): Unit = { + if (invocation.input.length != METHOD_ID_LENGTH + SubmitKeyRotationCmdInputDecoder.getABIDataParamsStaticLengthInBytes) { + throw new ExecutionRevertedException(s"Wrong invocation data field length: ${invocation.input.length}") + } else if (invocation.value.signum() != 0) { + throw new ExecutionRevertedException(s"Value is non-zero: $invocation") } } diff --git a/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala index 8826b61e1a..f5b4cf45db 100644 --- a/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/EoaMessageProcessor.scala @@ -2,8 +2,6 @@ package io.horizen.account.state import sparkz.util.SparkzLogging -import scala.compat.java8.OptionConverters.RichOptionalGeneric - /* * EoaMessageProcessor is responsible for management of regular coin transfers inside sidechain. * In our case to make a transfer from one user account (EOA account) to another user account. @@ -13,23 +11,21 @@ object EoaMessageProcessor extends MessageProcessor with SparkzLogging { // No actions required for transferring coins during genesis state initialization. } - override def canProcess(msg: Message, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { - // Can process only EOA to EOA transfer, so when "to" is an EOA account: - // There is no need to check "from" account because it can't be a smart contract one, - // because there is no known private key to create a valid signature. - // Note: in case of smart contract declaration "to" is null. - msg.getTo.asScala.exists(view.isEoaAccount) + override def customTracing(): Boolean = false + + override def canProcess(invocation: Invocation, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { + // Can process EOA to EOA and Native contract to EOA transfer + invocation.callee.exists(view.isEoaAccount) } @throws(classOf[ExecutionFailedException]) override def process( - msg: Message, + invocation: Invocation, view: BaseAccountStateView, - gas: GasPool, - blockContext: BlockContext + context: ExecutionContext ): Array[Byte] = { - view.subBalance(msg.getFrom, msg.getValue) - view.addBalance(msg.getTo.get(), msg.getValue) + view.subBalance(invocation.caller, invocation.value) + view.addBalance(invocation.callee.get, invocation.value) Array.emptyByteArray } } diff --git a/sdk/src/main/scala/io/horizen/account/state/ExecutionContext.scala b/sdk/src/main/scala/io/horizen/account/state/ExecutionContext.scala new file mode 100644 index 0000000000..18f71fca5c --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/ExecutionContext.scala @@ -0,0 +1,41 @@ +package io.horizen.account.state + +trait ExecutionContext { + /** + * The original message currently being processed + */ + val msg: Message + + /** + * Contextual information + */ + val blockContext: BlockContext + + /** + * Current call depth + */ + var depth: Int + + /** + * Manually advance call depth by given amount and continue execution with the given invocation. + * This is used to update the overall depth when returning from the EVM, in case multiple nested invocations happened + * there. + */ + @throws(classOf[InvalidMessageException]) + @throws(classOf[ExecutionFailedException]) + def executeDepth(invocation: Invocation, additionalDepth: Int): Array[Byte] = { + depth += additionalDepth + try { + execute(invocation) + } finally { + depth -= additionalDepth + } + } + + /** + * Process the given invocation within the current context + */ + @throws(classOf[InvalidMessageException]) + @throws(classOf[ExecutionFailedException]) + def execute(invocation: Invocation): Array[Byte] +} diff --git a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala index 86a41db93a..b936ecb34c 100644 --- a/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/ForgerStakeMsgProcessor.scala @@ -10,21 +10,22 @@ import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_ import io.horizen.account.state.events.{DelegateForgerStake, OpenForgerList, WithdrawForgerStake} import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_SMART_CONTRACT_ADDRESS import io.horizen.account.utils.ZenWeiConverter.isValidZenAmount +import io.horizen.evm.Address import io.horizen.params.NetworkParams import io.horizen.proof.Signature25519 import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} import io.horizen.utils.BytesUtils -import io.horizen.evm.Address import sparkz.crypto.hash.{Blake2b256, Keccak256} import java.math.BigInteger +import java.util.Optional import scala.collection.JavaConverters.seqAsJavaListConverter import scala.util.{Failure, Success, Try} trait ForgerStakesProvider { private[horizen] def getListOfForgersStakes(view: BaseAccountStateView): Seq[AccountForgingStakeInfo] - private[horizen] def addScCreationForgerStake(msg: Message, view: BaseAccountStateView): Array[Byte] + private[horizen] def addScCreationForgerStake(view: BaseAccountStateView, owner: Address, value: BigInteger, data: AddNewStakeCmdInput): Array[Byte] private[horizen] def findStakeData(view: BaseAccountStateView, stakeId: Array[Byte]): Option[ForgerStakeData] @@ -101,33 +102,53 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon view.removeAccountStorageBytes(contractAddress, stakeId) } + override def addScCreationForgerStake( + view: BaseAccountStateView, + owner: Address, + value: BigInteger, + data: AddNewStakeCmdInput + ): Array[Byte] = { + val msg = new Message( + owner, + Optional.of(contractAddress), + BigInteger.ZERO, + BigInteger.ZERO, + BigInteger.ZERO, + BigInteger.ZERO, + value, + // a negative nonce value will rule out collision with real transactions + BigInteger.ONE.negate(), + Bytes.concat(BytesUtils.fromHexString(AddNewStakeCmd), data.encode()), + false + ) + doAddNewStakeCmd(Invocation.fromMessage(msg), view, msg, isGenesisScCreation = true) + } - override def addScCreationForgerStake(msg: Message, view: BaseAccountStateView): Array[Byte] = - doAddNewStakeCmd(msg, view, isGenesisScCreation = true) - - def doAddNewStakeCmd(msg: Message, view: BaseAccountStateView, isGenesisScCreation: Boolean = false): Array[Byte] = { + def doAddNewStakeCmd(invocation: Invocation, view: BaseAccountStateView, msg: Message, isGenesisScCreation: Boolean = false): Array[Byte] = { // check that message contains a nonce, in the context of RPC calls the nonce might be missing if (msg.getNonce == null) { throw new ExecutionRevertedException("Call must include a nonce") } + val stakedAmount = invocation.value + // check that msg.value is greater than zero - if (msg.getValue.signum() <= 0) { + if (stakedAmount.signum() <= 0) { throw new ExecutionRevertedException("Value must not be zero") } // check that msg.value is a legal wei amount convertible to satoshis without any remainder - if (!isValidZenAmount(msg.getValue)) { - throw new ExecutionRevertedException(s"Value is not a legal wei amount: ${msg.getValue.toString()}") + if (!isValidZenAmount(stakedAmount)) { + throw new ExecutionRevertedException(s"Value is not a legal wei amount: ${stakedAmount.toString()}") } // check that sender account exists (unless we are staking in the sc creation phase) - if (!view.accountExists(msg.getFrom) && !isGenesisScCreation) { - throw new ExecutionRevertedException(s"Sender account does not exist: ${msg.getFrom}") + if (!view.accountExists(invocation.caller) && !isGenesisScCreation) { + throw new ExecutionRevertedException(s"Sender account does not exist: ${invocation.caller}") } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = AddNewStakeCmdInputDecoder.decode(inputParams) val blockSignPublicKey: PublicKey25519Proposition = cmdInput.forgerPublicKeys.blockSignPublicKey @@ -156,11 +177,10 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon } // add the obj to stateDb - val stakedAmount = msg.getValue addForgerStake(view, newStakeId, blockSignPublicKey, vrfPublicKey, ownerAddress, stakedAmount) log.debug(s"Added stake to stateDb: newStakeId=${BytesUtils.toHexString(newStakeId)}, blockSignPublicKey=$blockSignPublicKey, vrfPublicKey=$vrfPublicKey, ownerAddress=$ownerAddress, stakedAmount=$stakedAmount") - val addNewStakeEvt = DelegateForgerStake(msg.getFrom, ownerAddress, newStakeId, stakedAmount) + val addNewStakeEvt = DelegateForgerStake(invocation.caller, ownerAddress, newStakeId, stakedAmount) val evmLog = getEthereumConsensusDataLog(addNewStakeEvt) view.addLog(evmLog) @@ -170,7 +190,7 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon view.addBalance(contractAddress, stakedAmount) } else { // decrease the balance of `from` account by `tx.value` - view.subBalance(msg.getFrom, stakedAmount) + view.subBalance(invocation.caller, stakedAmount) // increase the balance of the "forger stake smart contract” account view.addBalance(contractAddress, stakedAmount) } @@ -178,10 +198,10 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon newStakeId } - private def checkGetListOfForgersCmd(msg: Message): Unit = { + private def checkGetListOfForgersCmd(calldata: Array[Byte]): Unit = { // check we have no other bytes after the op code in the msg data - if (getArgumentsFromData(msg.getData).length > 0) { - val msgStr = s"invalid msg data length: ${msg.getData.length}, expected $METHOD_ID_LENGTH" + if (getArgumentsFromData(calldata).length > 0) { + val msgStr = s"invalid msg data length: ${calldata.length}, expected $METHOD_ID_LENGTH" log.debug(msgStr) throw new ExecutionRevertedException(msgStr) } @@ -204,26 +224,26 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon AccountForgingStakeInfoListEncoder.encode(stakeList.asJava) } - def doGetListOfForgersCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { - if (msg.getValue.signum() != 0) { + def doGetListOfForgersCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } - checkGetListOfForgersCmd(msg) + checkGetListOfForgersCmd(invocation.input) doUncheckedGetListOfForgersStakesCmd(view) } - def doRemoveStakeCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { + def doRemoveStakeCmd(invocation: Invocation, view: BaseAccountStateView, msg: Message): Array[Byte] = { // check that message contains a nonce, in the context of RPC calls the nonce might be missing if (msg.getNonce == null) { throw new ExecutionRevertedException("Call must include a nonce") } - if (msg.getValue.signum() != 0) { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = RemoveStakeCmdInputDecoder.decode(inputParams) val stakeId: Array[Byte] = cmdInput.stakeId val signature: SignatureSecp256k1 = cmdInput.signature @@ -233,7 +253,7 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon .getOrElse(throw new ExecutionRevertedException("No such stake id in state-db")) // check signature - val msgToSign = getRemoveStakeCmdMessageToSign(stakeId, msg.getFrom, msg.getNonce.toByteArray) + val msgToSign = getRemoveStakeCmdMessageToSign(stakeId, invocation.caller, msg.getNonce.toByteArray) val isValid : Boolean = Try { signature.isValid(stakeData.ownerPublicKey, msgToSign) } match { @@ -279,7 +299,7 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon } } - def doOpenStakeForgerListCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { + def doOpenStakeForgerListCmd(invocation: Invocation, view: BaseAccountStateView, msg: Message): Array[Byte] = { if (!networkParams.restrictForgers) { throw new ExecutionRevertedException("Illegal call when list of forger is not restricted") @@ -289,11 +309,11 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon throw new ExecutionRevertedException("Illegal call when list of forger is empty") } - if (msg.getValue.signum() != 0) { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = OpenStakeForgerListCmdInputDecoder.decode(inputParams) val forgerIndex: Int = cmdInput.forgerIndex val signature: Signature25519 = cmdInput.signature @@ -307,7 +327,7 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon // check signature val blockSignerProposition = networkParams.allowedForgersList(forgerIndex)._1 - val msgToSign = getOpenStakeForgerListCmdMessageToSign(forgerIndex, msg.getFrom, msg.getNonce.toByteArray) + val msgToSign = getOpenStakeForgerListCmdMessageToSign(forgerIndex, invocation.caller, msg.getNonce.toByteArray) if (!signature.isValid(blockSignerProposition, msgToSign)) { throw new ExecutionRevertedException(s"Invalid signature, could not validate against blockSignerProposition=$blockSignerProposition") } @@ -329,7 +349,7 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon restrictForgerList(forgerIndex) = 1 view.updateAccountStorageBytes(contractAddress, RestrictedForgerFlagsList, restrictForgerList) - val addOpenStakeForgerListEvt = OpenForgerList(forgerIndex, msg.getFrom, blockSignerProposition) + val addOpenStakeForgerListEvt = OpenForgerList(forgerIndex, invocation.caller, blockSignerProposition) val evmLog = getEthereumConsensusDataLog(addOpenStakeForgerListEvt) view.addLog(evmLog) @@ -337,13 +357,13 @@ case class ForgerStakeMsgProcessor(params: NetworkParams) extends NativeSmartCon } @throws(classOf[ExecutionFailedException]) - override def process(msg: Message, view: BaseAccountStateView, gas: GasPool, blockContext: BlockContext): Array[Byte] = { - val gasView = view.getGasTrackedView(gas) - getFunctionSignature(msg.getData) match { - case GetListOfForgersCmd => doGetListOfForgersCmd(msg, gasView) - case AddNewStakeCmd => doAddNewStakeCmd(msg, gasView) - case RemoveStakeCmd => doRemoveStakeCmd(msg, gasView) - case OpenStakeForgerListCmd => doOpenStakeForgerListCmd(msg, gasView) + override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + val gasView = view.getGasTrackedView(invocation.gasPool) + getFunctionSignature(invocation.input) match { + case GetListOfForgersCmd => doGetListOfForgersCmd(invocation, gasView) + case AddNewStakeCmd => doAddNewStakeCmd(invocation, gasView, context.msg) + case RemoveStakeCmd => doRemoveStakeCmd(invocation, gasView, context.msg) + case OpenStakeForgerListCmd => doOpenStakeForgerListCmd(invocation, gasView, context.msg) case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") } } @@ -384,7 +404,7 @@ object ForgerStakeMsgProcessor { val GetListOfForgersCmd: String = getABIMethodId("getAllForgersStakes()") val AddNewStakeCmd: String = getABIMethodId("delegate(bytes32,bytes32,bytes1,address)") val RemoveStakeCmd: String = getABIMethodId("withdraw(bytes32,bytes1,bytes32,bytes32)") - val OpenStakeForgerListCmd: String = getABIMethodId("openStakeForgerList(uint32,bytes32,bytes32") + val OpenStakeForgerListCmd: String = getABIMethodId("openStakeForgerList(uint32,bytes32,bytes32") // ensure we have strings consistent with size of opcode require( diff --git a/sdk/src/main/scala/io/horizen/account/state/Invocation.scala b/sdk/src/main/scala/io/horizen/account/state/Invocation.scala new file mode 100644 index 0000000000..cc6970959f --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/Invocation.scala @@ -0,0 +1,100 @@ +package io.horizen.account.state + +import io.horizen.evm.{Address, TracerOpCode} +import io.horizen.utils.BytesUtils +import org.web3j.utils.Numeric + +import java.math.BigInteger +import scala.compat.java8.OptionConverters.RichOptionalGeneric + +case class Invocation( + caller: Address, + callee: Option[Address], + value: BigInteger, + input: Array[Byte], + gasPool: GasPool, + readOnly: Boolean, +) { + + /** + * Create nested invocation, similar to an EVM "CALL". + * + * @param addr + * address to call + * @param value + * amount of funds to transfer + * @param input + * calldata arguments + * @param gas + * amount of gas to allocate to the nested call, should not be greater than remaining gas in the parent invocation + * (will be validated in StateTransition.execute) + * @return + * new invocation object + */ + def call(addr: Address, value: BigInteger, input: Array[Byte], gas: BigInteger): Invocation = { + Invocation(callee.get, Some(addr), value, input, new GasPool(gas), readOnly = false) + } + + /** + * Create nested read-only invocation, similar to an EVM "STATICCALL". + * + * @param addr + * address to call + * @param input + * calldata arguments + * @param gas + * amount of gas to allocate to the nested call, should not be greater than remaining gas in the parent invocation + * (will be validated in StateTransition.execute) + * @return + */ + def staticCall(addr: Address, input: Array[Byte], gas: BigInteger): Invocation = { + Invocation(callee.get, Some(addr), BigInteger.ZERO, input, new GasPool(gas), readOnly = true) + } + + def guessOpCode(): TracerOpCode = { + if (callee.isEmpty) { + TracerOpCode.CREATE + } else if (readOnly) { + TracerOpCode.STATICCALL + } else { + TracerOpCode.CALL + } + } + + override def toString: String = + "%s{caller=%s, callee=%s, value=%s, input=%s, gasPool.getUsedGas=%s, readOnly=%s}" + .format( + this.getClass.toString, + caller.toString, + if (callee.isEmpty) "" else callee.get.toString, + if (value != null) Numeric.toHexStringWithPrefix(value) else "null", + if (input != null) BytesUtils.toHexString(input), + if (gasPool!=null) gasPool.getUsedGas.toString else "null", + if (readOnly) "YES" else "NO" + ) + + +// def traceTopLevel(tracer: Tracer): Unit = {} +} + +object Invocation { + + /** + * Create top level invocation from a message. + */ + def fromMessage(msg: Message, gasPool: GasPool): Invocation = + Invocation(msg.getFrom, msg.getTo.asScala, msg.getValue, msg.getData, gasPool, readOnly = false) + + /** + * Create top level invocation from a message, without any gas. Mostly useful for tests. + */ + def fromMessage(msg: Message): Invocation = + Invocation( + msg.getFrom, + msg.getTo.asScala, + msg.getValue, + msg.getData, + new GasPool(BigInteger.ZERO), + readOnly = false + ) +} diff --git a/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala index 7e1126e08f..f7c1b75054 100644 --- a/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/McAddrOwnershipMsgProcessor.scala @@ -4,7 +4,10 @@ import com.google.common.primitives.Bytes import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgumentsFromData, getFunctionSignature} import io.horizen.account.fork.ZenDAOFork import io.horizen.account.proof.SignatureSecp256k1 -import io.horizen.account.state.McAddrOwnershipMsgProcessor.{AddNewOwnershipCmd, GetListOfAllOwnershipsCmd, GetListOfOwnerScAddressesCmd, GetListOfOwnershipsCmd, OwnershipLinkedListNullValue, OwnershipsLinkedListTipKey, RemoveOwnershipCmd, ScAddressRefsLinkedListNullValue, ScAddressRefsLinkedListTipKey, ecParameters, getMcSignature, getOwnershipId, initDone, isForkActive} +import io.horizen.account.state.McAddrOwnershipMsgProcessor.{AddNewOwnershipCmd, GetListOfAllOwnershipsCmd, + GetListOfOwnerScAddressesCmd, GetListOfOwnershipsCmd, OwnershipLinkedListNullValue, OwnershipsLinkedListTipKey, + RemoveOwnershipCmd, ScAddressRefsLinkedListNullValue, ScAddressRefsLinkedListTipKey, ecParameters, getMcSignature, + getOwnershipId} import io.horizen.account.state.NativeSmartContractMsgProcessor.NULL_HEX_STRING_32 import io.horizen.account.state.events.{AddMcAddrOwnership, RemoveMcAddrOwnership} import io.horizen.account.utils.BigIntegerUInt256.getUnsignedByteArray @@ -52,35 +55,14 @@ trait McAddrOwnershipsProvider { * This can is useful for getting all mc addresses associated to an owner sc address without looping on the first * list (high gas consumption) */ -case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmartContractMsgProcessor with McAddrOwnershipsProvider { +case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmartContractWithFork + with McAddrOwnershipsProvider { override val contractAddress: Address = MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS override val contractCode: Array[Byte] = Keccak256.hash("McAddrOwnershipSmartContractCode") - override def init(view: BaseAccountStateView, consensusEpochNumber: Int): Unit = { - if (!isForkActive(consensusEpochNumber)) { - log.warn(s"Can not perform ${getClass.getName} initialization, fork is not active") - return - } - else { - if (initDone(view)) { - throw new MessageProcessorInitializationException("McAddrOwnership msg processor already initialized") - } - } - - // We do not call the parent init() method because it would throw an exception if the account already exists. - // In our case the initialization does not happen at genesis state, and someone might - // (on purpose or not) already have sent funds to the account, maybe from a deployed solidity smart contract or by means - // of an eoa transaction before fork activation - if (!view.accountExists(contractAddress)) { - log.debug(s"creating Message Processor account $contractAddress") - } else { - // TODO maybe we can check the balance at this point and transfer the amount somewhere - val errorMsg = s"Account $contractAddress already exists!! Overwriting account with contract code ${BytesUtils.toHexString(contractCode)}..." - log.warn(errorMsg) - } - view.addAccount(contractAddress, contractCode) + override protected def doSpecificInit(view: BaseAccountStateView, consensusEpochNumber: Int): Unit = { // set the initial value for the linked list last element (null hash) //------- // check if we have this key set to any value @@ -108,25 +90,6 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar log.warn(warnMsg) } view.updateAccountStorage(contractAddress, ScAddressRefsLinkedListTipKey, ScAddressRefsLinkedListNullValue) - - } - - override def canProcess(msg: Message, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { - if (super.canProcess(msg, view, consensusEpochNumber)) { - if (isForkActive(consensusEpochNumber)) { - // the gas cost of these calls is not taken into account in this case, we are not tracking gas consumption (and - // there is not an account to charge anyway) - if (!initDone(view)) - init(view, consensusEpochNumber) - true - } else { - // we can not handle anything before fork activation, but just warn if someone is trying to use it - log.warn(s"Can not process message in ${getClass.getName}, fork is not active: msg = $msg") - false - } - } else { - false - } } private def addMcAddrOwnership(view: BaseAccountStateView, ownershipId: Array[Byte], scAddress: Address, mcTransparentAddress: String): Unit = { @@ -217,7 +180,7 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar doubleSHA256Hash(Bytes.concat(mmb2, mts2)) } - def doAddNewOwnershipCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { + def doAddNewOwnershipCmd(invocation: Invocation, view: BaseAccountStateView, msg: Message): Array[Byte] = { // check that message contains a nonce, in the context of RPC calls the nonce might be missing if (msg.getNonce == null) { @@ -226,9 +189,9 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar throw new ExecutionRevertedException(errMsg) } - // check that msg.value is zero - if (msg.getValue.signum() != 0) { - val errMsg = s"Value must be zero: msg = $msg" + // check that invocation.value is zero + if (invocation.value.signum() != 0) { + val errMsg = s"Value must be zero: invocation = $invocation" log.warn(errMsg) throw new ExecutionRevertedException(errMsg) } @@ -240,7 +203,7 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar throw new ExecutionRevertedException(errMsg) } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = AddNewOwnershipCmdInputDecoder.decode(inputParams) val mcTransparentAddress = cmdInput.mcTransparentAddress @@ -252,7 +215,7 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar // verify the ownership validating the signature val mcSignSecp256k1: SignatureSecp256k1 = getMcSignature(mcSignature) if (!isValidOwnershipSignature(msg.getFrom, mcTransparentAddress, mcSignSecp256k1)) { - val errMsg = s"Invalid mc signature $mcSignature: msg = $msg" + val errMsg = s"Invalid mc signature $mcSignature: invocation = $invocation" log.warn(errMsg) throw new ExecutionRevertedException(errMsg) } @@ -261,7 +224,7 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar // to use many times a mc address he really owns getExistingAssociation(view, newOwnershipId) match { case Some(scAddrStr) => - val errMsg = s"MC address $mcTransparentAddress is already associated to sc address $scAddrStr: msg = $msg" + val errMsg = s"MC address $mcTransparentAddress is already associated to sc address $scAddrStr: invocation = $invocation" log.warn(errMsg) throw new ExecutionRevertedException(errMsg) case None => // do nothing @@ -280,14 +243,14 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar newOwnershipId } - def doRemoveOwnershipCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { + def doRemoveOwnershipCmd(invocation: Invocation, view: BaseAccountStateView, msg: Message): Array[Byte] = { // check that message contains a nonce, in the context of RPC calls the nonce might be missing if (msg.getNonce == null) { throw new ExecutionRevertedException("Call must include a nonce") } // check that msg.value is zero - if (msg.getValue.signum() != 0) { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Value must be zero") } @@ -296,7 +259,7 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar throw new ExecutionRevertedException(s"Sender account does not exist: ${msg.getFrom}") } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = RemoveOwnershipCmdInputDecoder.decode(inputParams) @@ -336,12 +299,12 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar } } - def doGetListOfOwnershipsCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { - if (msg.getValue.signum() != 0) { + def doGetListOfOwnershipsCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } - val inputParams = getArgumentsFromData(msg.getData) + val inputParams = getArgumentsFromData(invocation.input) val cmdInput = GetOwnershipsCmdInputDecoder.decode(inputParams) @@ -350,14 +313,14 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar } - def doGetListOfAllOwnershipsCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { - if (msg.getValue.signum() != 0) { + def doGetListOfAllOwnershipsCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } // check we have no other bytes after the op code in the msg data - if (getArgumentsFromData(msg.getData).length > 0) { - val msgStr = s"invalid msg data length: ${msg.getData.length}, expected $METHOD_ID_LENGTH" + if (getArgumentsFromData(invocation.input).length > 0) { + val msgStr = s"invalid msg data length: ${invocation.input.length}, expected $METHOD_ID_LENGTH" log.debug(msgStr) throw new ExecutionRevertedException(msgStr) } @@ -366,14 +329,14 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar McAddrOwnershipDataListEncoder.encode(ownershipList.asJava) } - def doGetListOfOwnerScAddressesCmd(msg: Message, view: BaseAccountStateView): Array[Byte] = { - if (msg.getValue.signum() != 0) { + def doGetListOfOwnerScAddressesCmd(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } // check we have no other bytes after the op code in the msg data - if (getArgumentsFromData(msg.getData).length > 0) { - val msgStr = s"invalid msg data length: ${msg.getData.length}, expected $METHOD_ID_LENGTH" + if (getArgumentsFromData(invocation.input).length > 0) { + val msgStr = s"invalid msg data length: ${invocation.input.length}, expected $METHOD_ID_LENGTH" log.debug(msgStr) throw new ExecutionRevertedException(msgStr) } @@ -470,32 +433,50 @@ case class McAddrOwnershipMsgProcessor(params: NetworkParams) extends NativeSmar } @throws(classOf[ExecutionFailedException]) - override def process(msg: Message, view: BaseAccountStateView, gas: GasPool, blockContext: BlockContext): Array[Byte] = { - if (!isForkActive(blockContext.consensusEpochNumber)) { + override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + if (!isForkActive(context.blockContext.consensusEpochNumber)) { throw new ExecutionRevertedException(s"zenDao fork not active") } - val gasView = view.getGasTrackedView(gas) + val gasView = view.getGasTrackedView(invocation.gasPool) if (!initDone(gasView)) { // should not happen since if the fork is active we should have perform the init at this point throw new ExecutionRevertedException(s"zenDao native smart contract init not done") } // this handles eoa2eoa too - if (msg.getData.length == 0) - throw new ExecutionRevertedException(s"No data in msg = $msg") + if (invocation.input.length == 0) + throw new ExecutionRevertedException(s"No data in invocation = $invocation") - getFunctionSignature(msg.getData) match { - case AddNewOwnershipCmd => doAddNewOwnershipCmd(msg, gasView) - case RemoveOwnershipCmd => doRemoveOwnershipCmd(msg, gasView) - case GetListOfAllOwnershipsCmd => doGetListOfAllOwnershipsCmd(msg, gasView) - case GetListOfOwnershipsCmd => doGetListOfOwnershipsCmd(msg, gasView) - case GetListOfOwnerScAddressesCmd => doGetListOfOwnerScAddressesCmd(msg, gasView) + getFunctionSignature(invocation.input) match { + case AddNewOwnershipCmd => doAddNewOwnershipCmd(invocation, gasView, context.msg) + case RemoveOwnershipCmd => doRemoveOwnershipCmd(invocation, gasView, context.msg) + case GetListOfAllOwnershipsCmd => doGetListOfAllOwnershipsCmd(invocation, gasView) + case GetListOfOwnershipsCmd => doGetListOfOwnershipsCmd(invocation, gasView) + case GetListOfOwnerScAddressesCmd => doGetListOfOwnerScAddressesCmd(invocation, gasView) case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") } } + override def initDone(view: BaseAccountStateView): Boolean = { + // depending on whether this is a warm or a cold access, this read op costs WarmStorageReadCostEIP2929 or ColdSloadCostEIP2929 + // gas units (currently defined as 100 ans 2100 resp.) + val initialTip = view.getAccountStorage(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS, OwnershipsLinkedListTipKey) + !initialTip.sameElements(NULL_HEX_STRING_32) + } + + override def isForkActive(consensusEpochNumber: Int): Boolean = { + val forkIsActive = ZenDAOFork.get(consensusEpochNumber).active + val strVal = if (forkIsActive) { + "YES" + } else { + "NO" + } + log.trace(s"Epoch $consensusEpochNumber: ZenDAO fork active=$strVal") + forkIsActive + } + } object McAddrOwnershipMsgProcessor extends SparkzLogging { @@ -544,18 +525,5 @@ object McAddrOwnershipMsgProcessor extends SparkzLogging { new SignatureSecp256k1(v, r, s) } - def initDone(view: BaseAccountStateView) : Boolean = { - // depending on whether this is a warm or a cold access, this read op costs WarmStorageReadCostEIP2929 or ColdSloadCostEIP2929 - // gas units (currently defined as 100 ans 2100 resp.) - val initialTip = view.getAccountStorage(MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS, OwnershipsLinkedListTipKey) - !initialTip.sameElements(NULL_HEX_STRING_32) - } - - def isForkActive(consensusEpochNumber: Integer): Boolean = { - val forkIsActive = ZenDAOFork.get(consensusEpochNumber).active - val strVal = if (forkIsActive) {"YES"} else {"NO"} - log.trace(s"Epoch $consensusEpochNumber: ZenDAO fork active=$strVal") - forkIsActive - } } diff --git a/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala b/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala index 8203e49baa..595c6d5bd9 100644 --- a/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala +++ b/sdk/src/main/scala/io/horizen/account/state/MessageProcessorUtil.scala @@ -2,7 +2,7 @@ package io.horizen.account.state import io.horizen.cryptolibprovider.CircuitTypes.{NaiveThresholdSignatureCircuit, NaiveThresholdSignatureCircuitWithKeyRotation} import io.horizen.evm.Address -import io.horizen.params.NetworkParams +import io.horizen.params.{NetworkParams, RegTestParams} import io.horizen.utils.BytesUtils import org.web3j.utils.Numeric import sparkz.core.serialization.{BytesSerializable, SparkzSerializer} @@ -17,17 +17,25 @@ object MessageProcessorUtil { case NaiveThresholdSignatureCircuit => None case NaiveThresholdSignatureCircuitWithKeyRotation => Some(CertificateKeyRotationMsgProcessor(params)) } - Seq( - // Since fork dependant native smart contract are not initialized at genesis state, their msg - // processor must be placed before the Eoa msg processor. - // This is for having the initialization performed as soon as the fork point is reached, otherwise - // the Eoa msg processor would preempt it - McAddrOwnershipMsgProcessor(params), - //-- - EoaMessageProcessor, - WithdrawalMsgProcessor, - ForgerStakeMsgProcessor(params), - ) ++ maybeKeyRotationMsgProcessor.toSeq ++ customMessageProcessors + + val maybeProxyMsgProcessor = params match { + case _ : RegTestParams => Some(ProxyMsgProcessor(params)) + case _ => None + } + + // Since fork dependant native smart contract are not initialized at genesis state, their msg + // processor must be placed before the Eoa msg processor. + // This is for having the initialization performed as soon as the fork point is reached, otherwise + // the Eoa msg processor would preempt it + + Seq(McAddrOwnershipMsgProcessor(params)) ++ + maybeProxyMsgProcessor.toSeq ++ + Seq(EoaMessageProcessor, + WithdrawalMsgProcessor, + ForgerStakeMsgProcessor(params), + ) ++ + maybeKeyRotationMsgProcessor.toSeq ++ + customMessageProcessors } diff --git a/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala index d5eed1b8ba..787ead611e 100644 --- a/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractMsgProcessor.scala @@ -6,8 +6,6 @@ import io.horizen.evm.Address import sparkz.crypto.hash.Keccak256 import sparkz.util.SparkzLogging -import scala.compat.java8.OptionConverters.RichOptionalGeneric - abstract class NativeSmartContractMsgProcessor extends MessageProcessor with SparkzLogging { val contractAddress: Address @@ -26,9 +24,11 @@ abstract class NativeSmartContractMsgProcessor extends MessageProcessor with Spa } } - override def canProcess(msg: Message, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { + override def customTracing(): Boolean = false + + override def canProcess(invocation: Invocation, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { // we rely on the condition that init() has already been called at this point - msg.getTo.asScala.exists(contractAddress.equals(_)) + invocation.callee.exists(contractAddress.equals(_)) } def getEthereumConsensusDataLog(event: Any): EthereumConsensusDataLog = { diff --git a/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractWithFork.scala b/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractWithFork.scala new file mode 100644 index 0000000000..22cc8b2444 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/NativeSmartContractWithFork.scala @@ -0,0 +1,66 @@ +package io.horizen.account.state + +import io.horizen.utils.BytesUtils + +/* + This abstract class should be used as base class, instead of NativeSmartContractMsgProcessor, when introducing a new + Message Processor implementing a native smart contract in an already existing blockchain. In fact, in this case, the + Message Processor is initialized and starts working only after having reached the specific fork point. + */ +abstract class NativeSmartContractWithFork extends NativeSmartContractMsgProcessor { + + + override def init(view: BaseAccountStateView, consensusEpochNumber: Int): Unit = { + if (isForkActive(consensusEpochNumber)) { + if (initDone(view)) { + log.error("Message processor already initialized") + throw new MessageProcessorInitializationException("Message processor already initialized") + } + // We do not call the parent init() method because it would throw an exception if the account already exists. + // In our case the initialization does not happen at genesis state, and someone might + // (on purpose or not) already have sent funds to the account, maybe from a deployed solidity smart contract or + // by means of an eoa transaction before fork activation + if (!view.accountExists(contractAddress)) { + log.debug(s"Creating Message Processor account $contractAddress") + } else { + // TODO maybe we can check the balance at this point and transfer the amount somewhere + val msg = s"Account $contractAddress already exists!! Overwriting account with contract code ${BytesUtils.toHexString(contractCode)}..." + log.warn(msg) + } + view.addAccount(contractAddress, contractCode) + + doSpecificInit(view, consensusEpochNumber) + + } + else + log.warn("Can not perform initialization, fork is not active") + } + + override def canProcess(invocation: Invocation, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { + if (super.canProcess(invocation, view, consensusEpochNumber)) { + if (isForkActive(consensusEpochNumber)) { + // the gas cost of these calls is not taken into account in this case, we are not tracking gas consumption (and + // there is not an account to charge anyway) + if (!initDone(view)) + init(view, consensusEpochNumber) + true + } else { + // we can not handle anything before fork activation, but just warn if someone is trying to use it + log.warn(s"Can not process invocation, fork is not active: invocation = $invocation") + false + } + } else + false + } + + + protected def doSpecificInit(view: BaseAccountStateView, consensusEpochNumber: Int): Unit = () + + def isForkActive(consensusEpochNumber: Int): Boolean = false + + def initDone(view: BaseAccountStateView): Boolean = { + view.accountExists(contractAddress) && contractCodeHash.sameElements(view.getCodeHash(contractAddress)) + } + + +} \ No newline at end of file diff --git a/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala new file mode 100644 index 0000000000..1da08cda43 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessor.scala @@ -0,0 +1,120 @@ +package io.horizen.account.state + +import io.horizen.account.abi.ABIUtil.{METHOD_ID_LENGTH, getABIMethodId, getArgumentsFromData, getFunctionSignature} +import io.horizen.account.fork.ContractInteroperabilityFork +import io.horizen.account.state.ProxyMsgProcessor._ +import io.horizen.account.state.events.ProxyInvocation +import io.horizen.account.utils.WellKnownAddresses.PROXY_SMART_CONTRACT_ADDRESS +import io.horizen.evm.Address +import io.horizen.params.{MainNetParams, NetworkParams, RegTestParams} +import io.horizen.utils.BytesUtils +import org.web3j.utils.Numeric +import sparkz.crypto.hash.Keccak256 + +/* + This Message Processor is used for testing invocations of EVM smart contracts from native smart contracts. + It can only be used when in regtest. + */ +case class ProxyMsgProcessor(params: NetworkParams) extends NativeSmartContractWithFork { + + override val contractAddress: Address = PROXY_SMART_CONTRACT_ADDRESS + override val contractCode: Array[Byte] = Keccak256.hash("ProxySmartContractCode") + + override def canProcess(invocation: Invocation, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { + params.isInstanceOf[RegTestParams] && super.canProcess(invocation, view, consensusEpochNumber) + } + + override def isForkActive(consensusEpochNumber: Int): Boolean = { + ContractInteroperabilityFork.get(consensusEpochNumber).active + } + + def doInvokeSmartContractStaticCallCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + doInvokeSmartContractCmd(invocation, view, context, readOnly = true) + } + + def doInvokeSmartContractCallCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + doInvokeSmartContractCmd(invocation, view, context, readOnly = false) + } + + private def doInvokeSmartContractCmd(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext, readOnly : Boolean): Array[Byte] = { + log.debug(s"Entering with invocation: $invocation") + + val value = invocation.value + + // check that invocation.value is greater or equal than zero + if (value.signum() < 0) { + throw new ExecutionRevertedException("Value must not be zero") + } + + // check that sender account exists + if (!view.accountExists(invocation.caller)) { + throw new ExecutionRevertedException(s"Sender account does not exist: ${invocation.caller}") + } + + val inputParams = getArgumentsFromData(invocation.input) + + val cmdInput = InvokeSmartContractCmdInputDecoder.decode(inputParams) + val contractAddress = cmdInput.contractAddress + val data = cmdInput.dataStr + + if (view.isEoaAccount(contractAddress)) { + throw new ExecutionRevertedException(s"smart contract address is an EOA") + } + + val dataBytes = Numeric.hexStringToByteArray(data) + val res = context.execute( + if (readOnly) { + log.debug(s"static call to smart contract, address=$contractAddress, data=$data") + invocation.staticCall( + contractAddress, + dataBytes, + invocation.gasPool.getGas // we use all the amount we currently have + ) + } else { + log.debug(s"call to smart contract, address=$contractAddress, data=$data") + invocation.call( + contractAddress, + value, + dataBytes, + invocation.gasPool.getGas // we use all the amount we currently have + ) + } + ) + + val proxyInvocationEvent = ProxyInvocation(invocation.caller, contractAddress, dataBytes) + val evmLog = getEthereumConsensusDataLog(proxyInvocationEvent) + view.addLog(evmLog) + + // result in case of success execution might be useful for RPC commands + log.debug(s"Exiting with res: ${BytesUtils.toHexString(res)}") + + res + } + + + @throws(classOf[ExecutionFailedException]) + override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + log.debug(s"processing invocation: $invocation") + + val gasView = view.getGasTrackedView(invocation.gasPool) + getFunctionSignature(invocation.input) match { + case InvokeSmartContractCallCmd => doInvokeSmartContractCallCmd(invocation, gasView, context) + case InvokeSmartContractStaticCallCmd => doInvokeSmartContractStaticCallCmd(invocation, gasView, context) + case opCodeHex => throw new ExecutionRevertedException(s"op code not supported: $opCodeHex") + } + } +} + +object ProxyMsgProcessor { + + val InvokeSmartContractCallCmd: String = getABIMethodId("invokeCall(address,bytes)") + val InvokeSmartContractStaticCallCmd: String = getABIMethodId("invokeStaticCall(address,bytes)") + + // ensure we have strings consistent with size of opcode + require( + InvokeSmartContractCallCmd.length == 2 * METHOD_ID_LENGTH && + InvokeSmartContractStaticCallCmd.length == 2 * METHOD_ID_LENGTH + ) + +} + diff --git a/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessorData.scala b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessorData.scala new file mode 100644 index 0000000000..69b2f9f418 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/ProxyMsgProcessorData.scala @@ -0,0 +1,49 @@ +package io.horizen.account.state + +import io.horizen.account.abi.{ABIDecoder, ABIEncodable, MsgProcessorInputDecoder} +import io.horizen.evm.Address +import io.horizen.utils.BytesUtils +import org.web3j.abi.TypeReference +import org.web3j.abi.datatypes.{DynamicBytes, DynamicStruct, Type, Address => AbiAddress} + +import java.util + +case class InvokeSmartContractCmdInput( + contractAddress: Address, + dataStr: String) extends ABIEncodable[DynamicStruct] { + + + override def asABIType(): DynamicStruct = { + + val dataBytes: Array[Byte] = org.web3j.utils.Numeric.hexStringToByteArray(dataStr) + val listOfParams: util.List[Type[_]] = util.Arrays.asList( + new AbiAddress(contractAddress.toString), + new DynamicBytes(dataBytes) + ) + new DynamicStruct(listOfParams) + } + + override def toString: String = "%s(contractAddress: %s, data: %s)" + .format(this.getClass.toString, contractAddress.toString, dataStr) +} + +object InvokeSmartContractCmdInputDecoder + extends ABIDecoder[InvokeSmartContractCmdInput] + with MsgProcessorInputDecoder[InvokeSmartContractCmdInput] { + + override val getListOfABIParamTypes: util.List[TypeReference[Type[_]]] = + org.web3j.abi.Utils.convert(util.Arrays.asList( + new TypeReference[AbiAddress]() {}, + new TypeReference[DynamicBytes]() {} + )) + + override def createType(listOfParams: util.List[Type[_]]): InvokeSmartContractCmdInput = { + val contractAddress = new Address(listOfParams.get(0).asInstanceOf[AbiAddress].toString) + val dataBytes = listOfParams.get(1).asInstanceOf[DynamicBytes].getValue + + InvokeSmartContractCmdInput(contractAddress, BytesUtils.toHexString(dataBytes)) + } + +} + + diff --git a/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala b/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala index 6bdbbb9015..c028a8927e 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateView.scala @@ -1,37 +1,34 @@ package io.horizen.account.state -import com.google.common.primitives.Bytes import io.horizen.SidechainTypes import io.horizen.account.proposition.AddressProposition import io.horizen.account.state.ForgerStakeMsgProcessor.AddNewStakeCmd import io.horizen.account.state.receipt.EthereumConsensusDataReceipt.ReceiptStatus import io.horizen.account.state.receipt.{EthereumConsensusDataLog, EthereumConsensusDataReceipt} import io.horizen.account.transaction.EthereumTransaction -import io.horizen.account.utils.WellKnownAddresses.FORGER_STAKE_SMART_CONTRACT_ADDRESS import io.horizen.account.utils.{BigIntegerUtil, MainchainTxCrosschainOutputAddressUtil, WellKnownAddresses, ZenWeiConverter} import io.horizen.block.{MainchainBlockReferenceData, MainchainTxForwardTransferCrosschainOutput, MainchainTxSidechainCreationCrosschainOutput} import io.horizen.certificatesubmitter.keys.{CertifiersKeys, KeyRotationProof, KeyRotationProofTypes} import io.horizen.consensus.ForgingStakeInfo +import io.horizen.evm.results.{EvmLog, ProofAccountResult} +import io.horizen.evm.{Address, Hash, ResourceHandle, StateDB} import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} import io.horizen.transaction.mainchain.{ForwardTransfer, SidechainCreation} import io.horizen.utils.BytesUtils -import io.horizen.evm.{Address, Hash, ResourceHandle, StateDB} -import io.horizen.evm.results.{EvmLog, ProofAccountResult} import sparkz.crypto.hash.Keccak256 import sparkz.util.SparkzLogging import java.math.BigInteger -import java.util.Optional import scala.collection.JavaConverters.asScalaBufferConverter import scala.util.Try class StateDbAccountStateView( stateDb: StateDB, - messageProcessors: Seq[MessageProcessor] + messageProcessors: Seq[MessageProcessor], + var readOnly: Boolean = false ) extends BaseAccountStateView with AutoCloseable with SparkzLogging { - lazy val withdrawalReqProvider: WithdrawalRequestProvider = messageProcessors.find(_.isInstanceOf[WithdrawalRequestProvider]).get.asInstanceOf[WithdrawalRequestProvider] lazy val forgerStakesProvider: ForgerStakesProvider = @@ -42,6 +39,10 @@ class StateDbAccountStateView( lazy val mcAddrOwnershipProvider: McAddrOwnershipsProvider = messageProcessors.find(_.isInstanceOf[McAddrOwnershipsProvider]).get.asInstanceOf[McAddrOwnershipsProvider] + lazy val listOfNativeSmartContractAddresses: Array[Address] = messageProcessors.collect { + case msgProcessor: NativeSmartContractMsgProcessor => msgProcessor.contractAddress + }.toArray + override def keyRotationProof(withdrawalEpoch: Int, indexOfSigner: Int, keyType: Int): Option[KeyRotationProof] = { certificateKeysProvider.getKeyRotationProof(withdrawalEpoch, indexOfSigner, KeyRotationProofTypes(keyType), this) } @@ -94,22 +95,7 @@ class StateDbAccountStateView( ) val cmdInput = AddNewStakeCmdInput(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddress) - val data = Bytes.concat(BytesUtils.fromHexString(AddNewStakeCmd), cmdInput.encode()) - - val message = new Message( - ownerAddress, - Optional.of(FORGER_STAKE_SMART_CONTRACT_ADDRESS), - BigInteger.ZERO, // gasPrice - BigInteger.ZERO, // gasFeeCap - BigInteger.ZERO, // gasTipCap - BigInteger.ZERO, // gasLimit - stakedAmount, - BigInteger.ONE.negate(), // a negative nonce value will rule out collision with real transactions - data, - false - ) - - val returnData = forgerStakesProvider.addScCreationForgerStake(message, this) + val returnData = forgerStakesProvider.addScCreationForgerStake(this, ownerAddress, stakedAmount, cmdInput) log.debug(s"sc creation forging stake added with stakeid: ${BytesUtils.toHexString(returnData)}") case ft: ForwardTransfer => @@ -168,7 +154,7 @@ class StateDbAccountStateView( @throws(classOf[InvalidMessageException]) @throws(classOf[ExecutionFailedException]) def applyMessage(msg: Message, blockGasPool: GasPool, blockContext: BlockContext): Array[Byte] = { - new StateTransition(this, messageProcessors, blockGasPool, blockContext).transition(msg) + new StateTransition(this, messageProcessors, blockGasPool, blockContext, msg).transition() } /** @@ -246,11 +232,15 @@ class StateDbAccountStateView( !stateDb.isEmpty(address) // account modifiers: - override def addAccount(address: Address, code: Array[Byte]): Unit = + override def addAccount(address: Address, code: Array[Byte]): Unit = { + if (readOnly) throw new WriteProtectionException("invalid account code change") stateDb.setCode(address, code) + } - override def increaseNonce(address: Address): Unit = + override def increaseNonce(address: Address): Unit = { + if (readOnly) throw new WriteProtectionException("invalid nonce change") stateDb.setNonce(address, getNonce(address).add(BigInteger.ONE)) + } @throws(classOf[ExecutionFailedException]) override def addBalance(address: Address, amount: BigInteger): Unit = { @@ -259,6 +249,7 @@ class StateDbAccountStateView( case x if x < 0 => throw new ExecutionFailedException("cannot add negative amount to balance") case _ => + if (readOnly) throw new WriteProtectionException("invalid balance change") stateDb.addBalance(address, amount) } } @@ -271,6 +262,7 @@ class StateDbAccountStateView( case x if x < 0 => throw new ExecutionFailedException("cannot subtract negative amount from balance") case _ => + if (readOnly) throw new WriteProtectionException("invalid balance change") // The check on the address balance to be sufficient to pay the amount at this point has already been // done by the state while validating the origin tx stateDb.subBalance(address, amount) @@ -280,8 +272,10 @@ class StateDbAccountStateView( override def getAccountStorage(address: Address, key: Array[Byte]): Array[Byte] = stateDb.getStorage(address, new Hash(key)).toBytes - override def updateAccountStorage(address: Address, key: Array[Byte], value: Array[Byte]): Unit = + override def updateAccountStorage(address: Address, key: Array[Byte], value: Array[Byte]): Unit = { + if (readOnly) throw new WriteProtectionException("invalid write access to storage") stateDb.setStorage(address, new Hash(key), new Hash(value)) + } final override def removeAccountStorage(address: Address, key: Array[Byte]): Unit = updateAccountStorage(address, key, Hash.ZERO.toBytes) @@ -306,6 +300,7 @@ class StateDbAccountStateView( } final override def updateAccountStorageBytes(address: Address, key: Array[Byte], value: Array[Byte]): Unit = { + if (readOnly) throw new WriteProtectionException("invalid write access to storage") // get previous length of value stored, if any val oldLength = new BigInteger(1, getAccountStorage(address, key)).intValueExact() // values are split up into 32-bytes chunks: @@ -369,5 +364,18 @@ class StateDbAccountStateView( def revertToSnapshot(revisionId: Int): Unit = stateDb.revertToSnapshot(revisionId) override def getGasTrackedView(gas: GasPool): BaseAccountStateView = - new StateDbAccountStateViewGasTracked(stateDb, messageProcessors, gas) + new StateDbAccountStateViewGasTracked(stateDb, messageProcessors, readOnly, gas) + + /** + * Prevent write access to account storage, balance, nonce and code. While write protection is enabled invalid access + * will throw a WriteProtectionException. + */ + def enableWriteProtection(): Unit = readOnly = true + + /** + * Disable write protection. + */ + def disableWriteProtection(): Unit = readOnly = false + + override def getNativeSmartContractAddressList(): Array[Address] = listOfNativeSmartContractAddresses } diff --git a/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateViewGasTracked.scala b/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateViewGasTracked.scala index 4da10f044b..25bcbb25c8 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateViewGasTracked.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateDbAccountStateViewGasTracked.scala @@ -10,8 +10,12 @@ import java.math.BigInteger * @param gas * GasPool instance to deduct gas from */ -class StateDbAccountStateViewGasTracked(stateDb: StateDB, messageProcessors: Seq[MessageProcessor], gas: GasPool) - extends StateDbAccountStateView(stateDb, messageProcessors) { +class StateDbAccountStateViewGasTracked( + stateDb: StateDB, + messageProcessors: Seq[MessageProcessor], + readOnly: Boolean, + gas: GasPool +) extends StateDbAccountStateView(stateDb, messageProcessors, readOnly) { /** * Consume gas for account access: diff --git a/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala b/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala index c32abb72d4..c1bc851c6c 100644 --- a/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala +++ b/sdk/src/main/scala/io/horizen/account/state/StateTransition.scala @@ -1,16 +1,31 @@ package io.horizen.account.state import io.horizen.account.utils.BigIntegerUtil +import io.horizen.evm.EvmContext import sparkz.util.SparkzLogging import java.math.BigInteger +import scala.collection.mutable.ListBuffer +import scala.jdk.OptionConverters.RichOptional +import scala.util.{Failure, Success, Try} class StateTransition( view: StateDbAccountStateView, messageProcessors: Seq[MessageProcessor], blockGasPool: GasPool, - blockContext: BlockContext - ) extends SparkzLogging { + val blockContext: BlockContext, + val msg: Message, + ) extends SparkzLogging with ExecutionContext { + + // the current stack of invocations + private val invocationStack = new ListBuffer[Invocation] + + // short hand to access the tracer + private val tracer = blockContext.getTracer.toScala + + // the current call depth, might be more than invocationStack.length of a message processor handled multiple levels, + // e.g. multiple internal calls withing the EVM + var depth = 0 /** * Perform a state transition by applying the given message to the current state view. Afterwards, the state will @@ -23,63 +38,45 @@ class StateTransition( */ @throws(classOf[InvalidMessageException]) @throws(classOf[ExecutionFailedException]) - def transition(msg: Message): Array[Byte] = { + def transition(): Array[Byte] = { // do preliminary checks preCheck(msg) // save the remaining block gas before any changes val initialBlockGas = blockGasPool.getGas // create a snapshot before any changes are made - val initialRevision = view.snapshot + val rollback = view.snapshot + var skipRefund = false + // allocate gas for processing this message + val gasPool = buyGas(msg) + // trace TX start + tracer.foreach(_.CaptureTxStart(gasPool.initialGas)) try { - // allocate gas for processing this message - val gasPool = buyGas(msg) // consume intrinsic gas val intrinsicGas = GasUtil.intrinsicGas(msg.getData, msg.getTo.isEmpty) if (gasPool.getGas.compareTo(intrinsicGas) < 0) throw IntrinsicGasException(gasPool.getGas, intrinsicGas) gasPool.subGas(intrinsicGas) // reset and prepare account access list view.setupAccessList(msg) - // find and execute the first matching processor - messageProcessors.find(_.canProcess(msg, view, blockContext.consensusEpochNumber)) match { - case None => - log.error(s"No message processor found for executing message $msg") - throw new IllegalArgumentException(s"No message processor found for executing message: $msg") - case Some(processor) => - // increase the nonce by 1 - view.increaseNonce(msg.getFrom) - // create a snapshot before any changes are made by the processor - val revisionProcessor = view.snapshot - var skipRefund = false - try { - processor.process(msg, view, gasPool, blockContext) - } catch { - // if the processor throws ExecutionRevertedException we revert changes - case err: ExecutionRevertedException => - view.revertToSnapshot(revisionProcessor) - throw err - // if the processor throws ExecutionFailedException we revert changes and consume any remaining gas - case err: ExecutionFailedException => - view.revertToSnapshot(revisionProcessor) - gasPool.subGas(gasPool.getGas) - throw err - case err : Throwable => - // do not process refunds in this case, all changes will be reverted - skipRefund = true - throw err - } finally { - if (!skipRefund) refundGas(msg, gasPool) - } - } + // increase the nonce by 1 + view.increaseNonce(msg.getFrom) + // execute top-level call frame + execute(Invocation.fromMessage(msg, gasPool)) } catch { // execution failed was already handled case err: ExecutionFailedException => throw err // any other exception will bubble up and invalidate the block - case err: Throwable => + case err: Exception => + // do not process refunds in this case, all changes will be reverted + skipRefund = true // revert all changes, even buying gas and increasing the nonce - view.revertToSnapshot(initialRevision) + view.revertToSnapshot(rollback) // revert any changes to the block gas pool blockGasPool.addGas(initialBlockGas.subtract(blockGasPool.getGas)) throw err + } finally { + if (!skipRefund) refundGas(msg, gasPool) + // trace TX end + tracer.foreach(_.CaptureTxEnd(gasPool.getGas)) } } @@ -146,4 +143,141 @@ class StateTransition( // return remaining gas to the gasPool of the current block so it is available for the next transaction blockGasPool.addGas(gas.getGas) } + + /** + * Execute given invocation on the current call stack. + */ + @throws(classOf[InvalidMessageException]) + @throws(classOf[ExecutionFailedException]) + def execute(invocation: Invocation): Array[Byte] = { + // limit call depth to 1024 + if (depth > 1024) throw new ExecutionRevertedException("max call depth exceeded") + // get caller gas pool, for the top-level call this is empty + // In case of callbacks from the EVM, this gas check could be wrong, because it is possible that the caller invocation + // wasn't added to the invocationStack. So, what will be found in invocationStack is a grand-parent invocation. + // As gas can only decrease, using a gaspool "too high" up the stack will only ever have "too much" gas, i.e. + // this will never throw a false out-of-gas error, but the gas limit might not be checked correctly. + // However, the EVM always makes sure that the gas passed to an inner call is less than the input gas, so it + // shouldn't be a problem (See https://eips.ethereum.org/EIPS/eip-150). + val callerGas = invocationStack.headOption.map(_.gasPool) + // allocate gas from caller to the nested invocation, this can throw if the caller does not have enough gas + callerGas.foreach(_.subGas(invocation.gasPool.getGas)) + // enable write protection if it is not already on + // every nested invocation after a read-only invocation must remain read-only + val firstReadOnlyInvocation = invocation.readOnly && !view.readOnly + if (firstReadOnlyInvocation) view.enableWriteProtection() + try { + // Verify that there is no value transfer during a read-only invocation. This would also throw later because + // view.addBalance and view.subBalance would throw, this just makes it fail faster. + if (invocation.readOnly && invocation.value.signum() != 0) { + throw new WriteProtectionException("invalid value transfer during read-only invocation") + } + // find and execute the first matching processor + messageProcessors.find(_.canProcess(invocation, view, blockContext.consensusEpochNumber)) match { + case None => + log.error(s"No message processor found for invocation: $invocation") + throw new IllegalArgumentException("Unable to execute invocation.") + case Some(processor) => invoke(processor, invocation) + } + } finally { + // disable write protection only if it was disabled before this invocation + if (firstReadOnlyInvocation) view.disableWriteProtection() + // return remaining gas to the caller + callerGas.foreach(_.addGas(invocation.gasPool.getGas)) + } + } + + private def invoke(processor: MessageProcessor, invocation: Invocation): Array[Byte] = { + val startTime = System.nanoTime() + if (!processor.customTracing()) { + tracer.foreach(tracer => { + if (depth == 0) { + // trace start of top-level call frame + val context = new EvmContext( + BigInteger.valueOf(blockContext.chainID), + blockContext.forgerAddress, + blockContext.blockGasLimit, + msg.getGasPrice, + BigInteger.valueOf(blockContext.blockNumber), + BigInteger.valueOf(blockContext.timestamp), + blockContext.baseFee, + blockContext.random) + tracer.CaptureStart( + view.getStateDbHandle, + context, + invocation.caller, + invocation.callee.orNull, + invocation.callee.isEmpty, + invocation.input, + invocation.gasPool.initialGas, + invocation.value + ) + } else { + // trace start of nested call frame + tracer.CaptureEnter( + invocation.guessOpCode(), + invocation.caller, + invocation.callee.orNull, + invocation.input, + invocation.gasPool.initialGas, + invocation.value + ) + } + }) + } + + // add new invocation to the stack + invocationStack.prepend(invocation) + // increase call depth + depth += 1 + // create a snapshot before any changes are made by the processor + val revert = view.snapshot + // execute the message processor + val result = Try.apply(processor.process(invocation, view, this)) + // handle errors + result match { + // if the processor throws ExecutionRevertedException we revert changes + case Failure(_: ExecutionRevertedException) => + view.revertToSnapshot(revert) + // if the processor throws ExecutionFailedException we revert changes and consume any remaining gas + case Failure(_: ExecutionFailedException) => + view.revertToSnapshot(revert) + invocation.gasPool.subGas(invocation.gasPool.getGas) + // other errors will be handled further up the stack + case _ => + } + // reduce call depth + depth -= 1 + // remove the current invocation from the stack + invocationStack.remove(0) + + if (!processor.customTracing()) { + tracer.foreach(tracer => { + val output = result match { + // revert reason returned from message processor + case Failure(err: ExecutionRevertedException) => err.returnData + // successful result + case Success(value) => value + // other errors do not have a return value + case _ => Array.emptyByteArray + } + // get error message if any + val error = result match { + case Failure(exception) => exception.getMessage + case _ => "" + } + if (depth == 0) { + // trace end of top-level call frame + tracer.CaptureEnd(output, invocation.gasPool.getUsedGas, System.nanoTime() - startTime, error) + } else { + // trace end of nested call frame + tracer.CaptureExit(output, invocation.gasPool.getUsedGas, error) + } + }) + } + + // note: this will either return the output of the message processor + // or rethrow any exception caused during execution + result.get + } } diff --git a/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala b/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala index e7786ea67e..a44351452f 100644 --- a/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala +++ b/sdk/src/main/scala/io/horizen/account/state/WithdrawalMsgProcessor.scala @@ -35,14 +35,14 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd val DustThresholdInWei: BigInteger = ZenWeiConverter.convertZenniesToWei(ZenCoinsUtils.getMinDustThreshold(ZenCoinsUtils.MC_DEFAULT_FEE_RATE)) @throws(classOf[ExecutionFailedException]) - override def process(msg: Message, view: BaseAccountStateView, gas: GasPool, blockContext: BlockContext): Array[Byte] = { - val gasView = view.getGasTrackedView(gas) - getFunctionSignature(msg.getData) match { + override def process(invocation: Invocation, view: BaseAccountStateView, context: ExecutionContext): Array[Byte] = { + val gasView = view.getGasTrackedView(invocation.gasPool) + getFunctionSignature(invocation.input) match { case GetListOfWithdrawalReqsCmdSig => - execGetListOfWithdrawalReqRecords(msg, gasView) + execGetListOfWithdrawalReqRecords(invocation, gasView) case AddNewWithdrawalReqCmdSig => - execAddWithdrawalRequest(msg, gasView, blockContext.withdrawalEpochNumber) + execAddWithdrawalRequest(invocation, gasView, context.blockContext.withdrawalEpochNumber) case functionSig => throw new ExecutionRevertedException(s"Requested function does not exist. Function signature: $functionSig") @@ -71,16 +71,16 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd listOfWithdrawalReqs } - protected def execGetListOfWithdrawalReqRecords(msg: Message, view: BaseAccountStateView): Array[Byte] = { - if (msg.getValue.signum() != 0) { + protected def execGetListOfWithdrawalReqRecords(invocation: Invocation, view: BaseAccountStateView): Array[Byte] = { + if (invocation.value.signum() != 0) { throw new ExecutionRevertedException("Call value must be zero") } - if (msg.getData.length != METHOD_ID_LENGTH + GetListOfWithdrawalRequestsCmdInputDecoder.getABIDataParamsLengthInBytes) - throw new ExecutionRevertedException(s"Wrong message data field length: ${msg.getData.length}") + if (invocation.input.length != METHOD_ID_LENGTH + GetListOfWithdrawalRequestsCmdInputDecoder.getABIDataParamsStaticLengthInBytes) + throw new ExecutionRevertedException(s"Wrong message data field length: ${invocation.input.length}") val inputParams : GetListOfWithdrawalRequestsCmdInput = Try { - GetListOfWithdrawalRequestsCmdInputDecoder.decode(getArgumentsFromData(msg.getData)) + GetListOfWithdrawalRequestsCmdInputDecoder.decode(getArgumentsFromData(invocation.input)) } match { case Success(decodedBytes) => decodedBytes case Failure(ex) => @@ -91,11 +91,11 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd WithdrawalRequestsListEncoder.encode(listOfWithdrawalReqs.asJava) } - private[horizen] def checkWithdrawalRequestValidity(msg: Message): Unit = { - val withdrawalAmount = msg.getValue + private[horizen] def checkWithdrawalRequestValidity(invocation: Invocation): Unit = { + val withdrawalAmount = invocation.value - if (msg.getData.length != METHOD_ID_LENGTH + AddWithdrawalRequestCmdInputDecoder.getABIDataParamsLengthInBytes) { - throw new ExecutionRevertedException(s"Wrong message data field length: ${msg.getData.length}") + if (invocation.input.length != METHOD_ID_LENGTH + AddWithdrawalRequestCmdInputDecoder.getABIDataParamsStaticLengthInBytes) { + throw new ExecutionRevertedException(s"Wrong message data field length: ${invocation.input.length}") } else if (!ZenWeiConverter.isValidZenAmount(withdrawalAmount)) { throw new ExecutionRevertedException(s"Withdrawal amount is not a valid Zen amount: $withdrawalAmount") } else if (withdrawalAmount.compareTo(DustThresholdInWei) < 0) { @@ -103,8 +103,8 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd } } - protected def execAddWithdrawalRequest(msg: Message, view: BaseAccountStateView, currentEpochNum: Int): Array[Byte] = { - checkWithdrawalRequestValidity(msg) + protected def execAddWithdrawalRequest(invocation: Invocation, view: BaseAccountStateView, currentEpochNum: Int): Array[Byte] = { + checkWithdrawalRequestValidity(invocation) val numOfWithdrawalReqs = getWithdrawalEpochCounter(view, currentEpochNum) if (numOfWithdrawalReqs >= MaxWithdrawalReqsNumPerEpoch) { throw new ExecutionRevertedException("Reached maximum number of Withdrawal Requests per epoch: request is invalid") @@ -113,16 +113,16 @@ object WithdrawalMsgProcessor extends NativeSmartContractMsgProcessor with Withd val nextNumOfWithdrawalReqs: Int = numOfWithdrawalReqs + 1 setWithdrawalEpochCounter(view, currentEpochNum, nextNumOfWithdrawalReqs) - val inputParams = AddWithdrawalRequestCmdInputDecoder.decode(getArgumentsFromData(msg.getData)) + val inputParams = AddWithdrawalRequestCmdInputDecoder.decode(getArgumentsFromData(invocation.input)) - val withdrawalAmount = msg.getValue + val withdrawalAmount = invocation.value val request = WithdrawalRequest(inputParams.mcAddr, withdrawalAmount) val requestInBytes = request.bytes view.updateAccountStorageBytes(contractAddress, getWithdrawalRequestsKey(currentEpochNum, nextNumOfWithdrawalReqs), requestInBytes) - view.subBalance(msg.getFrom, withdrawalAmount) + view.subBalance(invocation.caller, withdrawalAmount) - val withdrawalEvent = AddWithdrawalRequest(msg.getFrom, request.proposition, withdrawalAmount, currentEpochNum) + val withdrawalEvent = AddWithdrawalRequest(invocation.caller, request.proposition, withdrawalAmount, currentEpochNum) val evmLog = getEthereumConsensusDataLog(withdrawalEvent) view.addLog(evmLog) diff --git a/sdk/src/main/scala/io/horizen/account/state/events/ProxyInvocation.scala b/sdk/src/main/scala/io/horizen/account/state/events/ProxyInvocation.scala new file mode 100644 index 0000000000..cb81049929 --- /dev/null +++ b/sdk/src/main/scala/io/horizen/account/state/events/ProxyInvocation.scala @@ -0,0 +1,28 @@ +package io.horizen.account.state.events + +import io.horizen.account.state.events.annotation.{Indexed, Parameter} +import io.horizen.evm.Address +import org.web3j.abi.datatypes.generated.{Bytes32, Uint256} +import org.web3j.abi.datatypes.{DynamicBytes, Address => AbiAddress} + +import java.math.BigInteger +import scala.annotation.meta.getter + +case class ProxyInvocation( + @(Parameter @getter)(1) @(Indexed @getter) from: AbiAddress, + @(Parameter @getter)(2) @(Indexed @getter) to: AbiAddress, + @(Parameter @getter)(3) data: DynamicBytes +) + + +object ProxyInvocation { + def apply( + from: Address, + to: Address, + data: Array[Byte] + ): ProxyInvocation = ProxyInvocation( + new AbiAddress(from.toString), + new AbiAddress(to.toString), + new DynamicBytes(data) + ) +} \ No newline at end of file diff --git a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala index cf295a9b9f..7a82c12623 100644 --- a/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala +++ b/sdk/src/main/scala/io/horizen/account/storage/AccountStateMetadataStorageView.scala @@ -345,8 +345,10 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta case _ => false } if (isWithdrawalEpochSwitched) { - val certEpochNumberToRemove: Int = epochInfo.epoch - 4 - removeList.add(getTopQualityCertificateKey(certEpochNumberToRemove)) + getOldTopCertificatesToBeRemoved(epochInfo) match { + case Some(cert) => removeList.add(cert) + case _ => + } val blockFeeInfoEpochToRemove: Int = epochInfo.epoch - 1 for (counter <- 0 to getBlockFeeInfoCounter(blockFeeInfoEpochToRemove)) { @@ -371,6 +373,16 @@ class AccountStateMetadataStorageView(storage: Storage) extends AccountStateMeta } + private[storage] def getOldTopCertificatesToBeRemoved(epochInfo: WithdrawalEpochInfo): Option[ByteArrayWrapper] = { + val certEpochNumberToRemove: Int = epochInfo.epoch - 4 + // We only clean up the storage if the certEpochNumberToRemove has already been used as previous certificate hash + // in the certEpochNumberToRemove + 1 epoch certificate + if (storage.get(getTopQualityCertificateKey(certEpochNumberToRemove + 1)).isPresent) { + Some(getTopQualityCertificateKey(certEpochNumberToRemove)) + } else { + None + } + } private def getBlockFeeInfoCounter(withdrawalEpochNumber: Int): Int = { storage.get(getBlockFeeInfoCounterKey(withdrawalEpochNumber)).asScala match { diff --git a/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala b/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala index b72db35a21..abb038a8f8 100644 --- a/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala +++ b/sdk/src/main/scala/io/horizen/account/utils/WellKnownAddresses.scala @@ -9,5 +9,6 @@ object WellKnownAddresses { val FORGER_STAKE_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000022222222222222222222") val CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000044444444444444444444") val MC_ADDR_OWNERSHIP_SMART_CONTRACT_ADDRESS: Address = new Address("0x0000000000000000000088888888888888888888") + val PROXY_SMART_CONTRACT_ADDRESS: Address = new Address("0x00000000000000000000AAAAAAAAAAAAAAAAAAAA") } diff --git a/sdk/src/main/scala/io/horizen/block/MainchainBlockReference.scala b/sdk/src/main/scala/io/horizen/block/MainchainBlockReference.scala index 2110d6e46a..5ceea6e714 100644 --- a/sdk/src/main/scala/io/horizen/block/MainchainBlockReference.scala +++ b/sdk/src/main/scala/io/horizen/block/MainchainBlockReference.scala @@ -271,7 +271,9 @@ object MainchainBlockReference extends SparkzLogging { offset += certificatesCount.size() while (certificates.size < certificatesCount.value()) { - log.debug(s"Parse Mainchain certificate: ${BytesUtils.toHexString(util.Arrays.copyOfRange(mainchainBlockBytes, offset, mainchainBlockBytes.length))}") + log.whenDebugEnabled { + s"Parse Mainchain certificate: ${BytesUtils.toHexString(util.Arrays.copyOfRange(mainchainBlockBytes, offset, mainchainBlockBytes.length))}" + } val c: WithdrawalEpochCertificate = WithdrawalEpochCertificate.parse(mainchainBlockBytes, offset) certificates = certificates :+ c offset += c.size diff --git a/sdk/src/main/scala/io/horizen/certificatesubmitter/dataproof/CertificateDataWithKeyRotation.scala b/sdk/src/main/scala/io/horizen/certificatesubmitter/dataproof/CertificateDataWithKeyRotation.scala index 4c9f295f15..dce9ae53cf 100644 --- a/sdk/src/main/scala/io/horizen/certificatesubmitter/dataproof/CertificateDataWithKeyRotation.scala +++ b/sdk/src/main/scala/io/horizen/certificatesubmitter/dataproof/CertificateDataWithKeyRotation.scala @@ -32,7 +32,7 @@ case class CertificateDataWithKeyRotation(override val referencedEpochNumber: In } override def toString: String = { - "CertificateDataWithoutKeyRotation(" + + "CertificateDataWithKeyRotation(" + s"referencedEpochNumber = $referencedEpochNumber, " + s"sidechainId = ${sidechainId.mkString("Array(", ", ", ")")}, " + s"withdrawalRequests = {${backwardTransfers.mkString(",")}}, " + diff --git a/sdk/src/main/scala/io/horizen/fork/ConsensusParamsFork.scala b/sdk/src/main/scala/io/horizen/fork/ConsensusParamsFork.scala index 234ae4fb73..b2d912fa42 100644 --- a/sdk/src/main/scala/io/horizen/fork/ConsensusParamsFork.scala +++ b/sdk/src/main/scala/io/horizen/fork/ConsensusParamsFork.scala @@ -1,7 +1,5 @@ package io.horizen.fork -import io.horizen.utils.Pair - case class ConsensusParamsFork( consensusSlotsInEpoch: Int = 720, @@ -14,15 +12,4 @@ object ConsensusParamsFork { } val DefaultConsensusParamsFork: ConsensusParamsFork = ConsensusParamsFork() - - def getMaxPossibleSlotsEver(forks: java.util.List[Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]]): Int = { - //Get the max number of consensus slots per epoch from the App Fork configurator and use it to set the Storage versions to mantain - var maxConsensusSlotsInEpoch = ConsensusParamsFork.DefaultConsensusParamsFork.consensusSlotsInEpoch - forks.forEach(fork => { - if (fork.getValue.isInstanceOf[ConsensusParamsFork] && fork.getValue.asInstanceOf[ConsensusParamsFork].consensusSlotsInEpoch > maxConsensusSlotsInEpoch) { - maxConsensusSlotsInEpoch = fork.getValue.asInstanceOf[ConsensusParamsFork].consensusSlotsInEpoch - } - }) - maxConsensusSlotsInEpoch - } } diff --git a/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala b/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala index 874c1b2581..02610c5a38 100644 --- a/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala +++ b/sdk/src/main/scala/io/horizen/history/validation/ConsensusValidator.scala @@ -179,11 +179,15 @@ class ConsensusValidator[ //Verify that forging stake info in block is correct (including stake), exist in history and had enough stake to be forger private[horizen] def verifyForgingStakeInfo(header: SidechainBlockHeaderBase, stakeConsensusEpochInfo: StakeConsensusEpochInfo, vrfOutput: VrfOutput, percentageForkApplied: Boolean, activeSlotCoefficient: Double): Unit = { - log.debug(s"Verify Forging stake info against root hash: ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)} by merkle path ${header.forgingStakeMerklePath.bytes().deep.mkString}") + log.whenDebugEnabled { + s"Verify Forging stake info against root hash: ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)} by merkle path ${header.forgingStakeMerklePath.bytes().deep.mkString}" + } val forgingStakeIsCorrect = stakeConsensusEpochInfo.rootHash.sameElements(header.forgingStakeMerklePath.apply(header.forgingStakeInfo.hash)) if (!forgingStakeIsCorrect) { - log.debug(s"Actual stakeInfo: rootHash: ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)}, totalStake: ${stakeConsensusEpochInfo.totalStake}") + log.whenDebugEnabled { + s"Actual stakeInfo: rootHash: ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)}, totalStake: ${stakeConsensusEpochInfo.totalStake}" + } throw new IllegalStateException(s"Forging stake merkle path in block ${header.id} is inconsistent to stakes merkle root hash ${BytesUtils.toHexString(stakeConsensusEpochInfo.rootHash)}") } diff --git a/sdk/src/main/scala/io/horizen/utxo/SidechainNodeViewHolder.scala b/sdk/src/main/scala/io/horizen/utxo/SidechainNodeViewHolder.scala index 8a7252b2bf..835110a6c6 100644 --- a/sdk/src/main/scala/io/horizen/utxo/SidechainNodeViewHolder.scala +++ b/sdk/src/main/scala/io/horizen/utxo/SidechainNodeViewHolder.scala @@ -80,11 +80,14 @@ class SidechainNodeViewHolder(sidechainSettings: SidechainSettings, case Success(checkedState) => { val checkedStateVersion = checkedState.version - log.debug(s"history bestBlockId = ${historyVersion}, stateVersion = ${checkedStateVersion}") - - val height_h = restoredHistory.blockInfoById(restoredHistory.bestBlockId).height - val height_s = restoredHistory.blockInfoById(versionToId(checkedStateVersion)).height - log.debug(s"history height = ${height_h}, state height = ${height_s}") + log.whenDebugEnabled { + s"history bestBlockId = $historyVersion, stateVersion = $checkedStateVersion" + } + log.whenDebugEnabled { + val height_h = restoredHistory.blockInfoById(restoredHistory.bestBlockId).height + val height_s = restoredHistory.blockInfoById(versionToId(checkedStateVersion)).height + s"history height = $height_h, state height = $height_s" + } if (historyVersion == checkedStateVersion) { log.info("state and history storages are consistent") diff --git a/sdk/src/test/resources/nonce_calculation_hex b/sdk/src/test/resources/nonce_calculation_hex index eb9f371201..ada87b05af 100644 --- a/sdk/src/test/resources/nonce_calculation_hex +++ b/sdk/src/test/resources/nonce_calculation_hex @@ -1 +1 @@ -b57a927d64c58c1f \ No newline at end of file +88306472c9ce1668 \ No newline at end of file diff --git a/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala b/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala index d50160471e..a2fdaede18 100644 --- a/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala +++ b/sdk/src/test/scala/io/horizen/account/forger/AccountForgeMessageBuilderTest.scala @@ -400,7 +400,7 @@ class AccountForgeMessageBuilderTest Mockito .when( mockMsgProcessor.canProcess( - ArgumentMatchers.any[Message], + ArgumentMatchers.any[Invocation], ArgumentMatchers.any[AccountStateView], ArgumentMatchers.any[Int] ) @@ -409,10 +409,9 @@ class AccountForgeMessageBuilderTest Mockito .when( mockMsgProcessor.process( - ArgumentMatchers.any[Message], + ArgumentMatchers.any[Invocation], ArgumentMatchers.any[BaseAccountStateView], - ArgumentMatchers.any[GasPool], - ArgumentMatchers.any[BlockContext] + ArgumentMatchers.any[ExecutionContext] ) ) .thenThrow(new RuntimeException("kaputt")) diff --git a/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala b/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala index b61df9176f..ebaf2fe606 100644 --- a/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/AccountStateTest.scala @@ -6,7 +6,7 @@ import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork import io.horizen.account.storage.AccountStateMetadataStorage import io.horizen.account.transaction.EthereumTransaction import io.horizen.account.utils.{AccountBlockFeeInfo, AccountPayment} -import io.horizen.consensus.{ConsensusParamsUtil, intToConsensusEpochNumber} +import io.horizen.consensus.{ConsensusParamsUtil, intToConsensusEpochNumber, intToConsensusSlotNumber} import io.horizen.evm._ import io.horizen.fixtures.{SecretFixture, SidechainTypesTestsExtension, StoreFixture} import io.horizen.fork.{ConsensusParamsFork, ConsensusParamsForkInfo, ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch, SimpleForkConfigurator} @@ -38,11 +38,6 @@ class AccountStateTest var params: NetworkParams = mock[NetworkParams] val metadataStorage: AccountStateMetadataStorage = mock[AccountStateMetadataStorage] var state: AccountState = _ - ConsensusParamsUtil.setConsensusParamsForkActivation(Seq( - ConsensusParamsForkInfo(0, ConsensusParamsFork.DefaultConsensusParamsFork), - )) - ConsensusParamsUtil.setConsensusParamsForkTimestampActivation(Seq(TimeToEpochUtils.virtualGenesisBlockTimeStamp(params.sidechainGenesisBlockTimestamp))) - private def addMockBalance(account: Address, value: BigInteger) = { val stateDB = new StateDB(stateDbStorage, new Hash(metadataStorage.getAccountStateRoot)) stateDB.addBalance(account, value) @@ -60,12 +55,7 @@ class AccountStateTest val messageProcessors: Seq[MessageProcessor] = Seq() Mockito.when(params.chainId).thenReturn(1997) - Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(None) Mockito.when(metadataStorage.getAccountStateRoot).thenReturn(Hash.ZERO.toBytes) - ConsensusParamsUtil.setConsensusParamsForkActivation(Seq( - ConsensusParamsForkInfo(0, ConsensusParamsFork.DefaultConsensusParamsFork), - )) - ConsensusParamsUtil.setConsensusParamsForkTimestampActivation(Seq(TimeToEpochUtils.virtualGenesisBlockTimeStamp(params.sidechainGenesisBlockTimestamp))) state = new AccountState( params, @@ -186,6 +176,38 @@ class AccountStateTest assertEquals(state.isSwitchingConsensusEpoch(intToConsensusEpochNumber(currentEpochNumber.get)), true) } + @Test + def testSwitchingConsensusEpochsWith2Forks(): Unit = { + ConsensusParamsUtil.setConsensusParamsForkActivation(Seq( + ConsensusParamsForkInfo(0, ConsensusParamsFork.DefaultConsensusParamsFork), + ConsensusParamsForkInfo(20, ConsensusParamsFork(100,10)), + )) + ConsensusParamsUtil.setConsensusParamsForkTimestampActivation(Seq( + TimeToEpochUtils.virtualGenesisBlockTimeStamp(params.sidechainGenesisBlockTimestamp), //runs for 19 epochs + TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(20), intToConsensusSlotNumber(100)), //on the 20th epoch it's activated + )) + + // Test 1. check the first consensus params fork epochs match + Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Option(intToConsensusEpochNumber(11))) + // assert that block with this timestamp belongs to 11th epoch + var blockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(11), intToConsensusSlotNumber(720)).toInt + assertEquals(false, state.isSwitchingConsensusEpoch(intToConsensusEpochNumber(blockTimestamp))) + + + // Test 2. check the second consensus params fork epochs match + Mockito.when(metadataStorage.getConsensusEpochNumber).thenReturn(Option(intToConsensusEpochNumber(25))) + + // assert that block with this timestamp belongs to 25th epoch + blockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(25), intToConsensusSlotNumber(100)).toInt + assertEquals(false, state.isSwitchingConsensusEpoch(intToConsensusEpochNumber(blockTimestamp))) + + + // Test 3. check the second consensus params fork epochs don't match + // add another epoch to the timestamp and check if there is mismatch between epochs + blockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(26), intToConsensusSlotNumber(100)).toInt + assertEquals(true, state.isSwitchingConsensusEpoch(intToConsensusEpochNumber(blockTimestamp))) + } + @Test def testTransactionLimitExceedsBlockGasLimit(): Unit = { val tx = mock[EthereumTransaction] diff --git a/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala b/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala index 7daf12952c..220ba1f39a 100644 --- a/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/AccountStateViewTest.scala @@ -2,7 +2,7 @@ package io.horizen.account.state import io.horizen.account.AccountFixture import io.horizen.account.storage.AccountStateMetadataStorageView -import io.horizen.account.utils.ZenWeiConverter +import io.horizen.account.utils.{WellKnownAddresses, ZenWeiConverter} import io.horizen.fixtures.StoreFixture import io.horizen.params.NetworkParams import io.horizen.proposition.MCPublicKeyHashProposition @@ -103,4 +103,39 @@ class AccountStateViewTest extends JUnitSuite with MockitoSugar with MessageProc view.commit(bytesToVersion(getVersion.data())) } } + + @Test + def testGetNativeSmartContractAddressList(): Unit = { + var messageProcessors = Seq.empty[MessageProcessor] + val metadataStorageView = mock[AccountStateMetadataStorageView] + val stateDb = mock[StateDB] + var stateView = new AccountStateView(metadataStorageView, stateDb, messageProcessors) + + assertTrue("List of addresses is not empty", stateView.getNativeSmartContractAddressList().isEmpty) + + messageProcessors = Seq(mock[MessageProcessor]) + stateView = new AccountStateView(metadataStorageView, stateDb, messageProcessors) + assertTrue("List of addresses is not empty", stateView.getNativeSmartContractAddressList().isEmpty) + + val mockNativeSmartContract = mock[NativeSmartContractMsgProcessor] + val mockAddress = new Address("0x0000000000000000000011234561111111111111") + Mockito.when(mockNativeSmartContract.contractAddress).thenReturn(mockAddress) + messageProcessors = Seq(mockNativeSmartContract, mock[MessageProcessor]) + stateView = new AccountStateView(metadataStorageView, stateDb, messageProcessors) + var listOfAddresses = stateView.getNativeSmartContractAddressList() + assertEquals("Wrong list of addresses size", 1, listOfAddresses.length) + assertEquals("Wrong address", mockAddress, listOfAddresses.head) + + messageProcessors = Seq(EoaMessageProcessor, mockNativeSmartContract, + WithdrawalMsgProcessor, mock[MessageProcessor], new CertificateKeyRotationMsgProcessor(mockNetworkParams), mock[EvmMessageProcessor]) + stateView = new AccountStateView(metadataStorageView, stateDb, messageProcessors) + listOfAddresses = stateView.getNativeSmartContractAddressList() + assertEquals("Wrong list of addresses size", 3, listOfAddresses.length) + assertTrue("Missing mockNativeSmartContract address", listOfAddresses.contains(mockAddress)) + assertTrue("Missing WithdrawalMsgProcessor address", listOfAddresses.contains(WithdrawalMsgProcessor.contractAddress)) + assertTrue("Missing CertificateKeyRotationMsgProcessor address", listOfAddresses.contains(WellKnownAddresses.CERTIFICATE_KEY_ROTATION_SMART_CONTRACT_ADDRESS)) + + + } + } diff --git a/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala index 7f5844e453..55aafdbe66 100644 --- a/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/CertificateKeyRotationMsgProcessorTest.scala @@ -157,18 +157,14 @@ class CertificateKeyRotationMsgProcessorTest } } - withGas { - certificateKeyRotationMsgProcessor.process( - getMessage( - to = contractAddress, - data = BytesUtils.fromHexString(SubmitKeyRotationReqCmdSig) ++ encodedInput, - nonce = randomNonce - ), view, _, blockContext - ) - } + val msg = getMessage( + to = contractAddress, + data = BytesUtils.fromHexString(SubmitKeyRotationReqCmdSig) ++ encodedInput, + nonce = randomNonce + ) + withGas(TestContext.process(certificateKeyRotationMsgProcessor, msg, view, blockContext, _)) } - private def processBadKeyRotationMessage(newKey: SchnorrSecret, keyRotationProof: KeyRotationProof, view: AccountStateView, epoch: Int = 0, spuriousBytes: Option[Array[Byte]], badBytes: Option[Array[Byte]], errMsg : String) = { val ex = intercept[ExecutionRevertedException] { @@ -183,7 +179,6 @@ class CertificateKeyRotationMsgProcessorTest assertEquals("Wrong MethodId for SubmitKeyRotationReqCmdSig", "288d61cc", CertificateKeyRotationMsgProcessor.SubmitKeyRotationReqCmdSig) } - @Test def testProcessShortOpCode(): Unit = { usingView(certificateKeyRotationMsgProcessor) { view => @@ -237,7 +232,7 @@ class CertificateKeyRotationMsgProcessorTest when(mockNetworkParams.mastersPublicKeys).thenReturn(Seq(oldMasterKey.publicImage())) // negative test: try using an input with a trailing byte - processBadKeyRotationMessage(newMasterKey, keyRotationProof, view, spuriousBytes = Some(new Array[Byte](1)), badBytes = None, errMsg = "Wrong message data field length") + processBadKeyRotationMessage(newMasterKey, keyRotationProof, view, spuriousBytes = Some(new Array[Byte](1)), badBytes = None, errMsg = "Wrong invocation data field length") // negative test: try using a message with right length but wrong bytes val badBytes1 = Some(BytesUtils.fromHexString(notDecodableData)) processBadKeyRotationMessage(newMasterKey, keyRotationProof, view, spuriousBytes = None, badBytes = badBytes1, errMsg = "Could not decode") diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala new file mode 100644 index 0000000000..8149cf8834 --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropCallTest.scala @@ -0,0 +1,1028 @@ +package io.horizen.account.state + +import com.google.common.primitives.{Bytes, Ints} +import io.horizen.account.abi.ABIEncodable +import io.horizen.account.abi.ABIUtil.{getArgumentsFromData, getFunctionSignature} +import io.horizen.account.state.ContractInteropTestBase._ +import io.horizen.account.utils.BigIntegerUtil.toUint256Bytes +import io.horizen.account.utils.{FeeUtils, Secp256k1} +import io.horizen.evm._ +import io.horizen.utils.BytesUtils +import org.junit.Assert.{assertArrayEquals, assertEquals, fail} +import org.junit.Test +import org.scalatest.Assertions.intercept +import org.web3j.abi.{TypeEncoder, datatypes} +import org.web3j.abi.datatypes.generated.Uint256 +import org.web3j.abi.datatypes.{DynamicBytes, DynamicStruct, Type, Address => AbiAddress} +import sparkz.crypto.hash.Keccak256 + +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import scala.util.{Failure, Success, Try} + +class ContractInteropCallTest extends ContractInteropTestBase { + override val processorToTest: NativeSmartContractMsgProcessor = NativeTestContract + + + val WRITE_PROTECTION_ERR_MSG_FROM_EVM = "write protection" + val INVALID_OP_CODE__ERR_MSG_FROM_EVM = "invalid opcode" + val WRITE_PROTECTION_ERR_MSG_FROM_NATIVE_CONTRACT = "invalid write access to storage" + + private object NativeTestContract extends NativeSmartContractMsgProcessor { + override val contractAddress: Address = new Address("0x00000000000000000000000000000000deadbeef") + override val contractCode: Array[Byte] = Keccak256.hash("NativeTestContract") + + val STATICCALL_READONLY_TEST_SIG = "aaaaaaaa" + val STATICCALL_READWRITE_TEST_SIG = "bbbbbbbb" + val STATICCALL_READWRITE_WITH_TRY_TEST_SIG = "cccccccc" + val STATICCALL_NESTED_CALLS_TEST_SIG = "dddddddd" + val CREATE_TEST_SIG = "eeeeeeee" + + val COUNTER_KEY = Keccak256.hash("key".getBytes(StandardCharsets.UTF_8)) + + val SUB_CALLS_GAS: BigInteger = BigInteger.valueOf(25000) + val SUB_CALLS_GAS_HEX_STRING = "0x" + SUB_CALLS_GAS.toString(16) + + val DEPLOY_GAS: BigInteger = BigInteger.valueOf(320000) + val DEPLOY_GAS_HEX_STRING = "0x" + DEPLOY_GAS.toString(16) + + + override def process( + invocation: Invocation, + view: BaseAccountStateView, + context: ExecutionContext + ): Array[Byte] = { + val gasView = view.getGasTrackedView(invocation.gasPool) + //read method signature + getFunctionSignature(invocation.input) match { + case STATICCALL_READONLY_TEST_SIG => testStaticCallOnReadonlyMethod(invocation, context) + case NATIVE_CONTRACT_RETRIEVE_ABI_ID => retrieve(gasView) + case STATICCALL_READWRITE_TEST_SIG => testStaticCallOnReadwriteMethod(invocation, context) + case NATIVE_CONTRACT_INC_ABI_ID => inc(gasView) + case STATICCALL_READWRITE_WITH_TRY_TEST_SIG => testStaticCallOnReadwriteMethodWithTry(invocation, context) + case STATICCALL_NESTED_CALLS_TEST_SIG => testStaticCallNestedCalls(invocation, context) + case CREATE_TEST_SIG => testDeployContract(invocation, gasView, context) + case _ => throw new IllegalArgumentException("Unknown method call") + } + } + + def testStaticCallOnReadonlyMethod( + invocation: Invocation, + context: ExecutionContext + ): Array[Byte] = { + val evmContractAddress = new Address(getArgumentsFromData(invocation.input)) + + //read method signature + val externalMethod = BytesUtils.fromHexString(STORAGE_RETRIEVE_ABI_ID) + + // execute nested call to EVM contract + val res = context.execute(invocation.staticCall(evmContractAddress, externalMethod, SUB_CALLS_GAS)) + //Check that the statedb is readwrite again + context.execute(invocation.call(evmContractAddress, 0, BytesUtils.fromHexString(STORAGE_INC_ABI_ID), SUB_CALLS_GAS)) + res + } + + def testStaticCallOnReadwriteMethod( + invocation: Invocation, + context: ExecutionContext + ): Array[Byte] = { + val evmContractAddress = new Address(getArgumentsFromData(invocation.input)) + + //read method signature + val externalMethod = BytesUtils.fromHexString(STORAGE_INC_ABI_ID) + + // execute nested call to EVM contract + context.execute(invocation.staticCall(evmContractAddress, externalMethod, SUB_CALLS_GAS)) + } + + def testStaticCallNestedCalls( + invocation: Invocation, + context: ExecutionContext + ): Array[Byte] = { + val evmContractAddress = new Address(getArgumentsFromData(invocation.input)) + + //read method signature + val externalMethod = BytesUtils.fromHexString(NATIVE_CALLER_NESTED_ABI_ID) + + // execute nested call to EVM contract + context.execute(invocation.staticCall(evmContractAddress, externalMethod, BigInteger.valueOf(40000))) + } + + def testStaticCallOnReadwriteMethodWithTry( + invocation: Invocation, + context: ExecutionContext + ): Array[Byte] = { + val evmContractAddress = new Address(getArgumentsFromData(invocation.input)) + + //read method signature + val externalMethod = BytesUtils.fromHexString(STORAGE_INC_ABI_ID) + + // execute nested call to EVM contract. It should throw an exception but we continue with the transaction + Try(context.execute(invocation.staticCall(evmContractAddress, externalMethod, SUB_CALLS_GAS))) + + context.execute(invocation.call(evmContractAddress, 0, BytesUtils.fromHexString(STORAGE_INC_ABI_ID), SUB_CALLS_GAS)) + context.execute(invocation.staticCall(evmContractAddress, BytesUtils.fromHexString(STORAGE_RETRIEVE_ABI_ID), SUB_CALLS_GAS)) + } + + def testDeployContract( + invocation: Invocation, + view: BaseAccountStateView, + context: ExecutionContext + ): Array[Byte] = { + val evmContractCode = getArgumentsFromData(invocation.input) + + val createInvocation = Invocation(contractAddress, None, 0, evmContractCode, new GasPool(DEPLOY_GAS), readOnly = false) + // execute nested call to EVM contract + val res = context.execute(createInvocation) + res + } + + def retrieve(view: BaseAccountStateView): Array[Byte] = { + val counterInBytesPadded = view.getAccountStorage(contractAddress, COUNTER_KEY) + counterInBytesPadded + } + + def inc(view: BaseAccountStateView): Array[Byte] = { + val counterInBytesPadded = view.getAccountStorage(contractAddress, COUNTER_KEY) + var counter = org.web3j.utils.Numeric.toBigInt(counterInBytesPadded).intValueExact() + counter = counter + 1 + val newValue = org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(counter), 32) + view.updateAccountStorage(contractAddress, COUNTER_KEY, newValue) + newValue + } + + } + + @Test + def testNativeContractCallingEvmContract(): Unit = { + val initialValue = new BigInteger("400000000000000000000000000000000000000000000000000000000000002a", 16) + val initialValueBytes = toUint256Bytes(initialValue) + + // deploy the Storage contract (EVM based) and set the initial value + val storageContractAddress = deploy(ContractInteropTestBase.storageContractCode(initialValue)) + + /////////////////////////////////////////////////////// + // Test 1: Native contract executes a staticcall, that calls a readonly function on a Smart Contract + /////////////////////////////////////////////////////// + + // call the native contract and pass along the Storage contract address + val inputRetrieveRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_READONLY_TEST_SIG), + storageContractAddress.toBytes) + var returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + // verify that the NativeTestContract was able to call the retrieve() function on the EVM based contract + assertArrayEquals("unexpected result", initialValueBytes, returnData) + + // put a tracer into the context + var tracer = new Tracer(new TraceOptions(false, false, false, + false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + val returnDataTraced = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + // verify that the result is still correct. Calculate new expected value because before we called inc() + var currentExpectedValue = initialValue.add(BigInteger.ONE) + var currentExpectedValueBytes = toUint256Bytes(currentExpectedValue) + assertArrayEquals("unexpected result", currentExpectedValueBytes, returnDataTraced) + + var traceResult = tracer.getResult.result + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + val initialGasHexString = gasLimitHexString + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputRetrieveRequest)}", + "value": "0x0", + "output": "0x${BytesUtils.toHexString(currentExpectedValueBytes)}", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$storageContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "0xf6", + "input": "0x$STORAGE_RETRIEVE_ABI_ID", + "output": "0x${BytesUtils.toHexString(currentExpectedValueBytes)}" + }, { + "type" : "CALL", + "from" : "${NativeTestContract.contractAddress}", + "to" : "$storageContractAddress", + "value" : "0x0", + "gas" : "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed" : "0x1a4", + "input" : "0x$STORAGE_INC_ABI_ID", + "output" : "0x" + } ] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 2: Native contract executes a staticcall on a Smart Contract, calling a function that modifies the state. + // An exception is expected. + /////////////////////////////////////////////////////// + + val expectedErrorMsg = WRITE_PROTECTION_ERR_MSG_FROM_EVM + val inputIncRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_READWRITE_TEST_SIG), storageContractAddress.toBytes) + Try(transition(getMessage(NativeTestContract.contractAddress, data = inputIncRequest))) match { + case Failure(ex: ExecutionFailedException) => assertEquals("Wrong failed exception", expectedErrorMsg, ex.getMessage) + case _ => fail(s"Staticcall with readwrite method should have thrown a $expectedErrorMsg exception") + } + + //Check that the statedb wasn't changed => call again retrieve() + currentExpectedValue = currentExpectedValue.add(BigInteger.ONE) + currentExpectedValueBytes = toUint256Bytes(currentExpectedValue) + + returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + // verify that the NativeTestContract was able to call the retrieve() function on the EVM based contract + assertArrayEquals("unexpected result", currentExpectedValueBytes, returnData) + + // put a tracer into the context + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + currentExpectedValue = currentExpectedValue.add(BigInteger.ONE) + currentExpectedValueBytes = toUint256Bytes(currentExpectedValue) + Try(transition(getMessage(NativeTestContract.contractAddress, data = inputIncRequest))) + + traceResult = tracer.getResult.result + + val failedSubCallGasHexString = NativeTestContract.SUB_CALLS_GAS_HEX_STRING + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputIncRequest)}", + "error": "$expectedErrorMsg", + "value": "0x0", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$storageContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "$failedSubCallGasHexString", + "input": "0x$STORAGE_INC_ABI_ID", + "error": "$expectedErrorMsg" + }] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 3: Native contract executes a staticcall on a Smart Contract, calling a function that modifies the state. + // In this case the native smart contract catches the write protection exception and tries again to change the state + // calling a call instead of a static call. The transaction should succeed. + /////////////////////////////////////////////////////// + + currentExpectedValue = currentExpectedValue.add(BigInteger.ONE) + currentExpectedValueBytes = toUint256Bytes(currentExpectedValue) + val inputIncWithTryRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_READWRITE_WITH_TRY_TEST_SIG), storageContractAddress.toBytes) + returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputIncWithTryRequest)) + + assertArrayEquals("unexpected result", currentExpectedValueBytes, returnData) + + // put a tracer into the context + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + currentExpectedValue = currentExpectedValue.add(BigInteger.ONE) + currentExpectedValueBytes = toUint256Bytes(currentExpectedValue) + returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputIncWithTryRequest)) + assertArrayEquals("unexpected result", currentExpectedValueBytes, returnData) + + traceResult = tracer.getResult.result + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputIncWithTryRequest)}", + "output": "0x${BytesUtils.toHexString(currentExpectedValueBytes)}", + "value": "0x0", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$storageContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "$failedSubCallGasHexString", + "input": "0x$STORAGE_INC_ABI_ID", + "error": "$expectedErrorMsg" + }, + { + "type" : "CALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$storageContractAddress", + "value" : "0x0", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed" : "0x1a4", + "input" : "0x$STORAGE_INC_ABI_ID", + "output" : "0x" + }, + { + "type" : "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$storageContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed" : "0xf6", + "input" : "0x$STORAGE_RETRIEVE_ABI_ID", + "output": "0x${BytesUtils.toHexString(currentExpectedValueBytes)}" + } ] + }""", + traceResult + ) + + /////////////////////////////////////////////////////// + // Test 4: Native contract executes a staticcall on a Smart Contract, passing more gas than its input gas. + // The transaction should fails with an OutOfGas exception. + /////////////////////////////////////////////////////// + intercept[OutOfGasException] { + transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest), + gasLimit = NativeTestContract.SUB_CALLS_GAS.subtract(BigInteger.ONE)) + } + + } + + @Test + def testEvmContractCallingNativeContract(): Unit = { + val nativeCallerAddress = deploy(ContractInteropTestBase.nativeCallerContractCode) + + + /////////////////////////////////////////////////////// + // Test 1: Solidity contract executes a staticcall on a Native Smart Contract, before reaching the fork point. + // It should fail because the EVM tries to execute the "fake" code associated with the native smart contract + /////////////////////////////////////////////////////// + val retrieveInput = BytesUtils.fromHexString(NATIVE_CALLER_STATIC_READONLY_ABI_ID) + + var tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + val blockContextForFork = + new BlockContext(Address.ZERO, 0, FeeUtils.INITIAL_BASE_FEE, gasLimit, 1, 1, 1, 1234, null, Hash.ZERO) + blockContextForFork.setTracer(tracer) + + Try(transition(getMessage(nativeCallerAddress, data = retrieveInput), blockContextForFork)) match { + case Failure(ex: ExecutionRevertedException) => //OK + case res => fail(s"Wrong result: $res") + } + + var traceResult = tracer.getResult.result + // check tracer output + // Expected error from the EVM. The EVM doesn't know any native contract before the fork, it will treat them as EVM + // contracts: it will try to execute the "fake" code saved inside the stateDb and this causes an invalid opcode. + var expectedErrorMsg = "invalid opcode: opcode 0xce not defined" + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$nativeCallerAddress", + "gas": "$gasLimitHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(retrieveInput)}", + "value": "0x0", + "error": "execution reverted", + "calls": [{ + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "input": "0x$NATIVE_CONTRACT_RETRIEVE_ABI_ID", + "error": "$expectedErrorMsg" + }] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 2: Solidity contract executes a staticcall on a Native Smart Contract, calling a readonly function + // In the same call, it executes a call for incrementing the counter, to check that the statedb doesn't remain readonly + /////////////////////////////////////////////////////// + + var currentCounterValue = 0 + var returnData = transition(getMessage(nativeCallerAddress, data = retrieveInput)) + currentCounterValue = currentCounterValue + 1 + + var numericResult = org.web3j.utils.Numeric.toBigInt(returnData).intValueExact() + var expectedTxResult = 0 + assertEquals("Wrong result from first retrieve", expectedTxResult, numericResult) + + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + expectedTxResult = currentCounterValue + var returnDataTraced = transition(getMessage(nativeCallerAddress, data = retrieveInput)) + currentCounterValue = currentCounterValue + 1 + + // verify that the result is still correct. + var numericResultTraced = org.web3j.utils.Numeric.toBigInt(returnDataTraced).intValueExact() + assertEquals("Wrong result from first retrieve", expectedTxResult, numericResultTraced) + + traceResult = tracer.getResult.result + + var expectedTxOutputHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + var currentCounterValueHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(currentCounterValue), 32)) + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$nativeCallerAddress", + "gas": "$gasLimitHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(retrieveInput)}", + "value": "0x0", + "output": "$expectedTxOutputHex", + "calls": [{ + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "0x64", + "input": "0x$NATIVE_CONTRACT_RETRIEVE_ABI_ID", + "output": "$expectedTxOutputHex" + }, + { + "type": "CALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "value": "0x0", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "0xc8", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "output": "$currentCounterValueHex" + }] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 3: Solidity contract calls inc() method on Native Smart Contract, first using staticcall and then using call. + // Staticcall should fail but the transaction is not reverted because the Solidity contract doesn't check the staticcall result. + // call should works so the counter will be increment by 1. + /////////////////////////////////////////////////////// + + val readwriteInput = BytesUtils.fromHexString(NATIVE_CALLER_STATIC_READWRITE_ABI_ID) + + returnData = transition(getMessage(nativeCallerAddress, data = readwriteInput)) + numericResult = org.web3j.utils.Numeric.toBigInt(returnData).intValueExact() + currentCounterValue = currentCounterValue + 1 + expectedTxResult = currentCounterValue + assertEquals("Wrong result", expectedTxResult, numericResult) + + // Check that the counter inside the native smart contract didn't change + var result = transition(getMessage(nativeCallerAddress, data = retrieveInput)) + // verify that the result is still correct. + assertEquals("Wrong result from retrieve", currentCounterValue, org.web3j.utils.Numeric.toBigInt(result).intValueExact()) + currentCounterValue = currentCounterValue + 1 + + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + returnDataTraced = transition(getMessage(nativeCallerAddress, data = readwriteInput)) + currentCounterValue = currentCounterValue + 1 + expectedTxResult = currentCounterValue + // verify that the result is still correct. + numericResultTraced = org.web3j.utils.Numeric.toBigInt(returnDataTraced).intValueExact() + assertEquals("Wrong result from tracer call", expectedTxResult, numericResultTraced) + + traceResult = tracer.getResult.result + + expectedErrorMsg = WRITE_PROTECTION_ERR_MSG_FROM_NATIVE_CONTRACT + expectedTxOutputHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + val failedSubCallGasHexString = NativeTestContract.SUB_CALLS_GAS_HEX_STRING + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$nativeCallerAddress", + "gas": "$gasLimitHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(readwriteInput)}", + "value": "0x0", + "output": "$expectedTxOutputHex", + "calls": [{ + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "$failedSubCallGasHexString", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "error": "$expectedErrorMsg" + }, + { + "type": "CALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "value": "0x0", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "0xc8", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "output": "$expectedTxOutputHex" + }] + }""", + traceResult + ) + + /////////////////////////////////////////////////////// + // Test 4: Solidity contract calls a method on a Native Smart Contract using the contract interface. The method + // is declared in the contract interface as view but it actually is a readwrite function. + // The transaction should fail. + /////////////////////////////////////////////////////// + + val readwriteContractCallInput = BytesUtils.fromHexString(NATIVE_CALLER_STATIC_RW_CONTRACT_ABI_ID) + + Try(transition(getMessage(nativeCallerAddress, data = readwriteContractCallInput))) match { + case Failure(_: ExecutionRevertedException) => //OK + case res => fail(s"Wrong result: $res") + } + + // Check that the counter inside the native smart contract didn't change + result = transition(getMessage(nativeCallerAddress, data = retrieveInput)) + // verify that the result is still correct. + assertEquals("Wrong result from first retrieve", currentCounterValue, org.web3j.utils.Numeric.toBigInt(result).intValueExact()) + currentCounterValue = currentCounterValue + 1 + + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + Try(transition(getMessage(nativeCallerAddress, data = readwriteContractCallInput))) + + traceResult = tracer.getResult.result + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$nativeCallerAddress", + "gas": "$gasLimitHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(readwriteContractCallInput)}", + "value": "0x0", + "error" : "execution reverted", + "calls": [{ + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "$failedSubCallGasHexString", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "error": "$expectedErrorMsg" + }] + }""", + traceResult + ) + + /////////////////////////////////////////////////////// + // Test 5: In this test, the gas passed to the NativeCaller contract is less than the gas in turn passed in staticcall + // (that is 25000 gas). Even if the gas passed to the staticcall is greater than the gas available, this won't throw + // a OoG exception because the EVM in this case will just pass 63/64 of the available gas. + // See https://eips.ethereum.org/EIPS/eip-150 + /////////////////////////////////////////////////////// + + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + transition(getMessage(nativeCallerAddress, data = retrieveInput), + gasLimit = NativeTestContract.SUB_CALLS_GAS.subtract(BigInteger.ONE)) + + traceResult = tracer.getResult.result + + expectedTxResult += 1 + currentCounterValue += 1 + val inputGasHex = "0x" + NativeTestContract.SUB_CALLS_GAS.subtract(BigInteger.ONE).toString(16) + expectedTxOutputHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + currentCounterValueHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(currentCounterValue), 32)) + + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$nativeCallerAddress", + "gas": "$inputGasHex", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(retrieveInput)}", + "value": "0x0", + "output": "$expectedTxOutputHex", + "calls": [{ + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "0x5d77", + "gasUsed": "0x64", + "input": "0x$NATIVE_CONTRACT_RETRIEVE_ABI_ID", + "output": "$expectedTxOutputHex" + }, + { + "type": "CALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "value": "0x0", + "gas": "0x5a51", + "gasUsed": "0xc8", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "output": "$currentCounterValueHex" + }] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 6: Same as test 5 above but adding an additional smart contract (SimpleProxy) call in the stack. + // See https://eips.ethereum.org/EIPS/eip-150 + /////////////////////////////////////////////////////// + + // Deploying SimpleProxy contract. + // Change the nonce because in this test the part managing the nonce is skipped and it is need for creating the + // deployed contract address. + stateView.increaseNonce(origin) + val nonce = stateView.getNonce(origin) + stateView.increaseNonce(origin) + + transition(getMessage(null, data = ContractInteropTestBase.simpleProxyContractCode)) + + val simpleProxyAddress = Secp256k1.generateContractAddress(origin, nonce) + // Prepare the call + val invokeInput = CallSimpleContractCmdInput(nativeCallerAddress, BigInteger.ZERO, BytesUtils.toHexString(retrieveInput)) + val input = Bytes.concat(BytesUtils.fromHexString(SIMPLE_PROXY_CALL_ABI_ID), invokeInput.encode()) + + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + transition(getMessage(simpleProxyAddress, data = input), + gasLimit = NativeTestContract.SUB_CALLS_GAS.subtract(BigInteger.ONE)) + + traceResult = tracer.getResult.result + + expectedTxResult += 1 + currentCounterValue += 1 + expectedTxOutputHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + currentCounterValueHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(currentCounterValue), 32)) + val listOfParams: java.util.List[Type[_]] = java.util.Arrays.asList( + new datatypes.DynamicBytes(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + ) + + val output = TypeEncoder.encode(new DynamicStruct(listOfParams)) + + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "$simpleProxyAddress", + "gas": "$inputGasHex", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(input)}", + "value": "0x0", + "output": "0x$output", + "calls": [ + { + "type": "CALL", + "from": "$simpleProxyAddress", + "to": "$nativeCallerAddress", + "value": "0x0", + "gas": "0x5b43", + "gasUsed": "0x794", + "input": "0x${BytesUtils.toHexString(retrieveInput)}", + "output": "$expectedTxOutputHex", + "calls": [ + { + "type": "STATICCALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "gas": "0x572c", + "gasUsed": "0x64", + "input": "0x$NATIVE_CONTRACT_RETRIEVE_ABI_ID", + "output": "$expectedTxOutputHex" + }, + { + "type": "CALL", + "from": "$nativeCallerAddress", + "to": "${NativeTestContract.contractAddress}", + "value": "0x0", + "gas": "0x5407", + "gasUsed": "0xc8", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "output": "$currentCounterValueHex" + }] + }] + }""", + traceResult + ) + + } + + + + + @Test + def testNativeContractNestedCalls(): Unit = { + /////////////////////////////////////////////////////// + // In this test the native smart contracts executes a staticcall on a EVM base smart contract. The method called on + // the EVM smart contract in turn calls the inc() method on the original native smart contract. Because of the + // original staticcall, this last call fails. The EVM contract checks the result and reverts the transaction. + /////////////////////////////////////////////////////// + + // deploy the NativeCaller contract (EVM based) and set the initial value + val nativeCallerContractAddress = deploy(ContractInteropTestBase.nativeCallerContractCode) + + val input = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_NESTED_CALLS_TEST_SIG), nativeCallerContractAddress.toBytes) + + Try(transition(getMessage(NativeTestContract.contractAddress, data = input))) match { + case Failure(_: ExecutionRevertedException) => //OK + case e => fail(s"Should have failed with ExecutionRevertedException: $e") + } + + // put a tracer into the context + val tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + Try(transition(getMessage(NativeTestContract.contractAddress, data = input))) + + val traceResult = tracer.getResult.result + val invalidWriteErrorMsg = WRITE_PROTECTION_ERR_MSG_FROM_NATIVE_CONTRACT + + val failedSubCallGas = BigInteger.valueOf(25000) + val failedSubCallGasHexString = "0x" + failedSubCallGas.toString(16) + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$gasLimitHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(input)}", + "error": "execution reverted with return data \\"0x\\"", + "value": "0x0", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$nativeCallerContractAddress", + "gas": "0x9c40", + "gasUsed": "0x6e5e", + "input": "0x$NATIVE_CALLER_NESTED_ABI_ID", + "error": "execution reverted", + "calls" : [ { + "type" : "STATICCALL", + "from" : "$nativeCallerContractAddress", + "to" : "${NativeTestContract.contractAddress}", + "gas" : "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed" : "$failedSubCallGasHexString", + "input" : "0x$NATIVE_CONTRACT_INC_ABI_ID", + "error" : "$invalidWriteErrorMsg" + } ] + }] + }""", + traceResult + ) + } + + + @Test + def testNativeContractCallingNativeContract(): Unit = { + + val destNativeContractAddress = NativeTestContract.contractAddress + + /////////////////////////////////////////////////////// + // Test 1: Native contract executes a staticcall on a Native Smart Contract, calling a readonly function + /////////////////////////////////////////////////////// + var currentCounterValue = 0 + var expectedTxResult = currentCounterValue + + // call a native contract and pass along the native contract address + val inputRetrieveRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_READONLY_TEST_SIG), destNativeContractAddress.toBytes) + var returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + currentCounterValue = currentCounterValue + 1 + var numericResult = org.web3j.utils.Numeric.toBigInt(returnData).intValueExact() + + // verify that the NativeTestContract was able to call the retrieve() function on the native contract + assertEquals("unexpected result", expectedTxResult, numericResult) + + // put a tracer into the context + var tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + expectedTxResult = currentCounterValue + // repeat the call again + var returnDataTraced = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + // verify that the result is still correct. Calculate new expected value because before we called inc() + currentCounterValue = currentCounterValue + 1 + val expectedTxOutputHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(expectedTxResult), 32)) + val currentCounterValueHex = "0x" + BytesUtils.toHexString(org.web3j.utils.Numeric.toBytesPadded(BigInteger.valueOf(currentCounterValue), 32)) + + var traceResult = tracer.getResult.result + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + val initialGasHexString = gasLimitHexString + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputRetrieveRequest)}", + "value": "0x0", + "output": "$expectedTxOutputHex", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$destNativeContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "0x64", + "input": "0x$NATIVE_CONTRACT_RETRIEVE_ABI_ID", + "output": "$expectedTxOutputHex" + }, { + "type" : "CALL", + "from" : "${NativeTestContract.contractAddress}", + "to" : "$destNativeContractAddress", + "value" : "0x0", + "gas" : "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed" : "0xc8", + "input" : "0x$NATIVE_CONTRACT_INC_ABI_ID", + "output": "$currentCounterValueHex" + } ] + }""", + traceResult + ) + + + /////////////////////////////////////////////////////// + // Test 2: Native contract executes a staticcall on a native Contract, calling a function that modifies the state + // An exception is expected. + /////////////////////////////////////////////////////// + + val expectedErrorMsg = WRITE_PROTECTION_ERR_MSG_FROM_NATIVE_CONTRACT + val inputIncRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.STATICCALL_READWRITE_TEST_SIG), destNativeContractAddress.toBytes) + Try(transition(getMessage(NativeTestContract.contractAddress, data = inputIncRequest))) match { + case Failure(ex: ExecutionFailedException) => assertEquals("Wrong failed exception", expectedErrorMsg, ex.getMessage) + case _ => fail("Staticcall with readwrite method should have thrown a " + expectedErrorMsg + " exception") + } + + //Check that the statedb wasn't changed. + + returnData = transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest)) + expectedTxResult = currentCounterValue + currentCounterValue = currentCounterValue + 1 + numericResult = org.web3j.utils.Numeric.toBigInt(returnData).intValueExact() + + // verify that the NativeTestContract was able to call the retrieve() function on the native contract + assertEquals("unexpected result", expectedTxResult, numericResult) + + // put a tracer into the context + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + currentCounterValue = currentCounterValue + 1 + Try(transition(getMessage(NativeTestContract.contractAddress, data = inputIncRequest))) + + traceResult = tracer.getResult.result + + val failedSubCallGasHexString = NativeTestContract.SUB_CALLS_GAS_HEX_STRING + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputIncRequest)}", + "error": "$expectedErrorMsg", + "value": "0x0", + "calls": [{ + "type": "STATICCALL", + "from": "${NativeTestContract.contractAddress}", + "to": "$destNativeContractAddress", + "gas": "${NativeTestContract.SUB_CALLS_GAS_HEX_STRING}", + "gasUsed": "$failedSubCallGasHexString", + "input": "0x$NATIVE_CONTRACT_INC_ABI_ID", + "error": "$expectedErrorMsg" + }] + }""", + traceResult + ) + + /////////////////////////////////////////////////////// + // Test 3: Native contract deploys a smart contract + /////////////////////////////////////////////////////// + + // call a native contract and pass along the native contract address + val initialNonce = stateView.getNonce(NativeTestContract.contractAddress) + val inputCreateRequest = Bytes.concat(BytesUtils.fromHexString(NativeTestContract.CREATE_TEST_SIG), ContractInteropTestBase.nativeCallerContractCode) + transition(getMessage(NativeTestContract.contractAddress, data = inputCreateRequest)) + + //Check that the smart contract was actually deployed + val contractAddress = Secp256k1.generateContractAddress(NativeTestContract.contractAddress, initialNonce) + val retrieveInput = BytesUtils.fromHexString(NATIVE_CALLER_STATIC_READONLY_ABI_ID) + + transition(getMessage(contractAddress, data = retrieveInput)) + + //Check that the nonce of he native smart contract was incremented + val currentNonce = stateView.getNonce(NativeTestContract.contractAddress) + assertEquals("Wrong nonce after deploying a contract", initialNonce.add(1), currentNonce) + + // put a tracer into the context + tracer = new Tracer(new TraceOptions(false, false, false, false, "callTracer", null)) + blockContext.setTracer(tracer) + + // repeat the call again + returnDataTraced = transition(getMessage(NativeTestContract.contractAddress, data = inputCreateRequest)) + traceResult = tracer.getResult.result + + val deployedContractAddress = Secp256k1.generateContractAddress(NativeTestContract.contractAddress, currentNonce) + val inputContractCodeHexString = s"0x${BytesUtils.toHexString(ContractInteropTestBase.nativeCallerContractCode)}" + val outputContractCodeHexString = s"0x${BytesUtils.toHexString(stateView.getCode(deployedContractAddress))}" + + // The gasUsed is empty since the traceTxStart and txTxEnd are not called in this unit test + // check tracer output + assertJsonEquals( + s"""{ + "type": "CALL", + "from": "$origin", + "to": "${NativeTestContract.contractAddress}", + "gas": "$initialGasHexString", + "gasUsed": "", + "input": "0x${BytesUtils.toHexString(inputCreateRequest)}", + "value": "0x0", + "output": "0x", + "calls": [{ + "type" : "CREATE", + "from" : "${NativeTestContract.contractAddress}", + "to" : "$deployedContractAddress", + "value" : "0x0", + "gas" : "${NativeTestContract.DEPLOY_GAS_HEX_STRING}", + "gasUsed" : "0x4c493", + "input" : "$inputContractCodeHexString", + "output": "$outputContractCodeHexString" + } ] + }""", + traceResult + ) + + /////////////////////////////////////////////////////// + // Test 4: Native contract executes a staticcall on a native contract, passing more gas than its input gas. + // The transaction should fails with an OutOfGas exception. + /////////////////////////////////////////////////////// + intercept[OutOfGasException] { + transition(getMessage(NativeTestContract.contractAddress, data = inputRetrieveRequest), + gasLimit = NativeTestContract.SUB_CALLS_GAS.subtract(BigInteger.ONE)) + } + + } + +} + +case class CallSimpleContractCmdInput( + contractAddress: Address, + value: BigInteger, + dataStr: String) extends ABIEncodable[DynamicStruct] { + + + override def asABIType(): DynamicStruct = { + + val dataBytes: Array[Byte] = org.web3j.utils.Numeric.hexStringToByteArray(dataStr) + val listOfParams: java.util.List[Type[_]] = java.util.Arrays.asList( + new AbiAddress(contractAddress.toString), + new Uint256(value), + new DynamicBytes(dataBytes) + ) + new DynamicStruct(listOfParams) + } +} diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala new file mode 100644 index 0000000000..b6b59acd21 --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropStackTest.scala @@ -0,0 +1,114 @@ +package io.horizen.account.state + +import io.horizen.evm.{Address, TraceOptions, Tracer} +import io.horizen.utils.BytesUtils +import org.junit.Assert.assertEquals +import org.junit.Test +import sparkz.crypto.hash.Keccak256 + +import java.nio.ByteBuffer +import scala.util.Try + +class ContractInteropStackTest extends ContractInteropTestBase { + + override val processorToTest: NativeSmartContractMsgProcessor = NativeTestContract + + /** + * Native Contract to test correct call depth limiting: The input is parsed into three arguments + * - function signature: ignored here, but useful in combination with an EVM-based contract + * - target: address of the next contract to call + * - counter: to keep track of maximum reached call depth + * + * Execution causes one nested call to the given target address with the input set to: + * - function signature: passed through unchanged + * - target: the contracts own address + * - counter: incremented and passed along + * + * The return value is: + * - The result of the nested call in case of success. + * - The current counter if the nested call failed. + * + * Note this enables multiple test cases: + * - Passing the contract address itself as target causes an infinite recursive loop. + * - Passing a different target address allows that contract to call back to this one to cause a loop between two + * contracts. + */ + private object NativeTestContract extends NativeSmartContractMsgProcessor { + override val contractAddress: Address = new Address("0x00000000000000000000000000000000deadbeef") + override val contractCode: Array[Byte] = Keccak256.hash("NativeInteropStackContract") + + override def process( + invocation: Invocation, + view: BaseAccountStateView, + context: ExecutionContext + ): Array[Byte] = { + // parse input + val in = invocation.input + if (in.length != 4 + 32 + 32) { + throw new IllegalArgumentException("NativeInteropStackContract called with invalid arguments") + } + val target = new Address(in.slice(16, 36)) + val counter = bytesToInt(in.slice(64, 68)) + assertEquals("unexpected call depth", context.depth - 1, counter) + // execute nested call + val nestedInput = abiEncode(contractAddress, counter + 1) + val nestedGas = invocation.gasPool.getGas.divide(64).multiply(63) + val result = Try.apply(context.execute(invocation.staticCall(target, nestedInput, nestedGas))) + // return result or the current counter in case the nested call failed + result.getOrElse(new Array[Byte](28) ++ intToBytes(counter)) + } + } + + private def intToBytes(num: Int) = ByteBuffer.allocate(4).putInt(num).array() + private def bytesToInt(arr: Array[Byte]) = BytesUtils.getInt(arr, arr.length - 4) + + private def abiEncode(target: Address, counter: Int = 0) = { + funcSignature ++ new Array[Byte](12) ++ target.toBytes ++ new Array[Byte](28) ++ intToBytes(counter) + } + + // function signature of loop(address,uint32) + private val funcSignature = BytesUtils.fromHexString("e08b6262") + + @Test + def testNativeCallDepth(): Unit = { + // call a native contract and cause recursive loop by making the contract call itself in a loop + val returnData = + transition(getMessage(NativeTestContract.contractAddress, data = abiEncode(NativeTestContract.contractAddress))) + // at call depth 1024 we expect the call to fail + // as the function returns the maximum call depth reached we expect 1024 + val callDepthReached = bytesToInt(returnData) + println("infinite loop returned: " + callDepthReached) + assertEquals("unexpected call depth", 1024, callDepthReached) + } + + @Test + def testEvmCallDepth(): Unit = { + val address = deploy(ContractInteropTestBase.nativeInteropContractCode) + // call an EVM-based contract and cause recursive loop by making the contract call itself in a loop + val returnData = transition(getMessage(address, data = abiEncode(address))) + // at call depth 1024 we expect the call to fail + // as the function returns the maximum call depth reached we expect 1024 + val callDepthReached = bytesToInt(returnData) + println("infinite loop returned: " + callDepthReached) + assertEquals("unexpected call depth", 1024, callDepthReached) + } + + @Test + def testInteropCallDepth(): Unit = { + val address = deploy(ContractInteropTestBase.nativeInteropContractCode) + // cause a call loop: native contract => EVM-based contract => native contract => ... + val tracer = new Tracer(new TraceOptions(false, false, false, + false, "callTracer", null)) + blockContext.setTracer(tracer) + + val returnData = + transition(getMessage(NativeTestContract.contractAddress, data = abiEncode(address))) + + // cause a call loop: EVM-based contract => native contract => EVM-based contract => ... + // at call depth 1024 we expect the call to fail + // as the function returns the maximum call depth reached we expect 1024 + val callDepthReached = bytesToInt(returnData) + println("infinite loop returned: " + callDepthReached) + assertEquals("unexpected call depth", 1024, callDepthReached) + } +} diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala new file mode 100644 index 0000000000..eb528399c5 --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTestBase.scala @@ -0,0 +1,134 @@ +package io.horizen.account.state + +import io.horizen.account.abi.ABIUtil.getABIMethodId +import io.horizen.account.fork.ContractInteroperabilityFork +import io.horizen.account.utils.BigIntegerUtil.toUint256Bytes +import io.horizen.account.utils.{FeeUtils, Secp256k1} +import io.horizen.evm._ +import io.horizen.fork.{ForkConfigurator, ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch} +import io.horizen.utils.{BytesUtils, Pair} +import org.junit.{After, Before} + +import java.math.BigInteger +import java.util +import scala.jdk.CollectionConverters.seqAsJavaListConverter + +abstract class ContractInteropTestBase extends MessageProcessorFixture { + + val Interop_Fork_Point = 50 + val initialBalance = new BigInteger("2000000000000") + + // note: the gas limit has to be ridiculously high for some tests to reach the maximum call depth of 1024 because of + // the 63/64 rule when passing gas to a nested call: + // - The remaining fraction of gas at depth 1024 is: (63/64)^1024 + // - The lower limit to have 10k gas available at depth 1024 is: 10k / (63/64)^1024 = ~100 billion + // - Some gas is consumed along the way, so we give x10: 1 trillion + val gasLimit: BigInteger = BigInteger.TEN.pow(12) + val gasLimitHexString = s"0x${gasLimit.toString(16)}" + + val blockContext = + new BlockContext(Address.ZERO, 0, FeeUtils.INITIAL_BASE_FEE, gasLimit, 1, Interop_Fork_Point, 1, 1234, null, Hash.ZERO) + + /** + * Derived tests have to supply a native contract to the test setup. + */ + val processorToTest: NativeSmartContractMsgProcessor + + private var processors: Seq[MessageProcessor] = _ + private var db: Database = _ + protected var stateView: AccountStateView = _ + + @Before + def setup(): Unit = { + class TestOptionalForkConfigurator extends ForkConfigurator { + override val fork1activation: SidechainForkConsensusEpoch = SidechainForkConsensusEpoch(0, 0, 0) + + override def getOptionalSidechainForks: util.List[Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]] = + Seq[Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]]( + new Pair(SidechainForkConsensusEpoch(Interop_Fork_Point, Interop_Fork_Point, Interop_Fork_Point), ContractInteroperabilityFork(true)), + ).asJava + } + + ForkManagerUtil.initializeForkManager(new TestOptionalForkConfigurator(), "regtest") + processors = Seq( + EoaMessageProcessor, + processorToTest, + new EvmMessageProcessor() + ) + db = new MemoryDatabase() + stateView = new AccountStateView(metadataStorageView, new StateDB(db, Hash.ZERO), processors) + stateView.addBalance(origin, initialBalance) + processorToTest.init(stateView, 0) + // by default start with fork active + } + + @After + def cleanup(): Unit = { + stateView.close() + db.close() + blockContext.removeTracer() + } + + protected def deploy(evmContractCode: Array[Byte]): Address = { + val nonce = stateView.getNonce(origin) + // deploy the NativeInterop contract (EVM based) + transition(getMessage(null, data = evmContractCode)) + // get deployed contract address + Secp256k1.generateContractAddress(origin, nonce) + } + + protected def transition(msg: Message, blckContext: BlockContext = blockContext, gasLimit: BigInteger = gasLimit): Array[Byte] = { + val transition = new StateTransition(stateView, processors, new GasPool(gasLimit), blckContext, msg) + transition.execute(Invocation.fromMessage(msg, new GasPool(gasLimit))) + } +} + +object ContractInteropTestBase { + // compiled EVM byte-code of NativeInterop contract + // source: libevm/native/test/NativeInterop.sol + def nativeInteropContractCode: Array[Byte] = BytesUtils.fromHexString( + "6080604052600080546001600160a01b031916692222222222222222222217905534801561002c57600080fd5b506107048061003c6000396000f3fe60806040526004361061004e5760003560e01c806367a7dbb41461005a5780637d286e4814610071578063b63fc52914610084578063cb14b856146100af578063e08b6262146100c457600080fd5b3661005557005b600080fd5b34801561006657600080fd5b5061006f6100f9565b005b61006f61007f3660046103d4565b61019e565b34801561009057600080fd5b50610099610245565b6040516100a691906103f8565b60405180910390f35b3480156100bb57600080fd5b5061006f6102c9565b3480156100d057600080fd5b506100e46100df366004610495565b61031e565b60405163ffffffff90911681526020016100a6565b6000805460408051600481526024810182526020810180516001600160e01b031663f6ad3c2360e01b179052905183926001600160a01b0316916127109161014191906104ce565b6000604051808303818686f4925050503d806000811461017d576040519150601f19603f3d011682016040523d82523d6000602084013e610182565b606091505b50909250905081151560000361019a57805160208201fd5b5050565b6000816001600160a01b03163460405160006040518083038185875af1925050503d80600081146101eb576040519150601f19603f3d011682016040523d82523d6000602084013e6101f0565b606091505b505090508061019a5760405162461bcd60e51b815260206004820152601860248201527f6661696c656420746f207472616e736665722076616c75650000000000000000604482015260640160405180910390fd5b606060008054906101000a90046001600160a01b03166001600160a01b031663f6ad3c236127106040518263ffffffff1660e01b81526004016000604051808303818786fa15801561029b573d6000803e3d6000fd5b50505050506040513d6000823e601f3d908101601f191682016040526102c4919081019061056d565b905090565b60008054604080517ff6ad3c23f0605b9ed84e6ad346e341d181873063303443c922270a3f389ee85e80825260048083019093526001600160a01b03909316939091602091839190829087612710f250505050565b60006001600160a01b03831663e08b62623061033b85600161067f565b6040516001600160e01b031960e085901b1681526001600160a01b03909216600483015263ffffffff1660248201526044016020604051808303816000875af19250505080156103a8575060408051601f3d908101601f191682019092526103a5918101906106b1565b60015b6103b35750806103b6565b90505b92915050565b6001600160a01b03811681146103d157600080fd5b50565b6000602082840312156103e657600080fd5b81356103f1816103bc565b9392505050565b602080825282518282018190526000919060409081850190868401855b82811015610476578151805185528681015187860152858101516001600160a01b031686860152606080820151908601526080808201519086015260a0908101516001600160f81b0319169085015260c09093019290850190600101610415565b5091979650505050505050565b63ffffffff811681146103d157600080fd5b600080604083850312156104a857600080fd5b82356104b3816103bc565b915060208301356104c381610483565b809150509250929050565b6000825160005b818110156104ef57602081860181015185830152016104d5565b506000920191825250919050565b634e487b7160e01b600052604160045260246000fd5b60405160c0810167ffffffffffffffff81118282101715610536576105366104fd565b60405290565b604051601f8201601f1916810167ffffffffffffffff81118282101715610565576105656104fd565b604052919050565b6000602080838503121561058057600080fd5b825167ffffffffffffffff8082111561059857600080fd5b818501915085601f8301126105ac57600080fd5b8151818111156105be576105be6104fd565b6105cc848260051b0161053c565b818152848101925060c09182028401850191888311156105eb57600080fd5b938501935b828510156106735780858a0312156106085760008081fd5b610610610513565b85518152868601518782015260408087015161062b816103bc565b90820152606086810151908201526080808701519082015260a0808701516001600160f81b0319811681146106605760008081fd5b90820152845293840193928501926105f0565b50979650505050505050565b63ffffffff8181168382160190808211156106aa57634e487b7160e01b600052601160045260246000fd5b5092915050565b6000602082840312156106c357600080fd5b81516103f18161048356fea264697066735822122062599b043536bb9eb9f7572b2e7a5f215cfa0d6c9fca05ca97d98fa995a4cd8064736f6c63430008140033" + ) + + // compiled EVM byte-code of the Storage contract, + // source: libevm/native/test/Storage.sol + // note: the constructor parameter is appended at the end + def storageContractCode(initialValue: BigInteger): Array[Byte] = BytesUtils.fromHexString( + "608060405234801561001057600080fd5b5060405161023638038061023683398101604081905261002f916100f6565b6000819055604051339060008051602061021683398151915290610073906020808252600c908201526b48656c6c6f20576f726c642160a01b604082015260600190565b60405180910390a2336001600160a01b03166000805160206102168339815191526040516100bf906020808252600a908201526948656c6c6f2045564d2160b01b604082015260600190565b60405180910390a26040517ffe1a3ad11e425db4b8e6af35d11c50118826a496df73006fc724cb27f2b9994690600090a15061010f565b60006020828403121561010857600080fd5b5051919050565b60f98061011d6000396000f3fe60806040526004361060305760003560e01c80632e64cec1146035578063371303c01460565780636057361d14606a575b600080fd5b348015604057600080fd5b5060005460405190815260200160405180910390f35b348015606157600080fd5b506068607a565b005b606860753660046086565b600055565b6000546075906001609e565b600060208284031215609757600080fd5b5035919050565b6000821982111560be57634e487b7160e01b600052601160045260246000fd5b50019056fea264697066735822122080d9db531d29b1bd6b4e16762726b70e2a94f0b40ee4e2ab534d9b879cf1c25664736f6c634300080f00330738f4da267a110d810e6e89fc59e46be6de0c37b1d5cd559b267dc3688e74e0" + ) ++ toUint256Bytes(initialValue) + + val STORAGE_RETRIEVE_ABI_ID: String = getABIMethodId("retrieve()") + + val STORAGE_INC_ABI_ID: String = getABIMethodId("inc()") + + + // compiled EVM byte-code of NativeCaller contract + // source: libevm/native/test/NativeCaller.sol + def nativeCallerContractCode: Array[Byte] = BytesUtils.fromHexString( + "6080604052600080546001600160a01b03191663deadbeef17905534801561002657600080fd5b506105aa806100366000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c806326afe20e146100515780634560bfed14610072578063ee880d141461007a578063ff01556b14610082575b600080fd5b61005961008a565b60405163ffffffff909116815260200160405180910390f35b610059610145565b6100596102d0565b610059610497565b6000805460408051600481526024810182526020810180516001600160e01b031662dc4c0f60e61b17905290516001600160a01b03909216918391829184916161a8916100d79190610518565b60006040518083038160008787f1925050503d8060008114610115576040519150601f19603f3d011682016040523d82523d6000602084013e61011a565b606091505b50915091508161012957600080fd5b8080602001905181019061013d9190610547565b935050505090565b6000805460408051600481526024810182526020810180516001600160e01b0316632e64cec160e01b17905290516001600160a01b03909216918391829184916161a8916101939190610518565b6000604051808303818686fa925050503d80600081146101cf576040519150601f19603f3d011682016040523d82523d6000602084013e6101d4565b606091505b50915091506000818060200190518101906101ef9190610547565b60408051600481526024810182526020810180516001600160e01b031662dc4c0f60e61b179052905191925060009182916001600160a01b038816916161a89161023891610518565b60006040518083038160008787f1925050503d8060008114610276576040519150601f19603f3d011682016040523d82523d6000602084013e61027b565b606091505b5091509150816102c55760405162461bcd60e51b815260206004820152601060248201526f63616c6c2073686f756c6420776f726b60801b60448201526064015b60405180910390fd5b509095945050505050565b6000805460408051600481526024810182526020810180516001600160e01b031662dc4c0f60e61b17905290516001600160a01b03909216918391829184916161a89161031d9190610518565b6000604051808303818686fa925050503d8060008114610359576040519150601f19603f3d011682016040523d82523d6000602084013e61035e565b606091505b509150915081156103aa5760405162461bcd60e51b815260206004820152601660248201527514dd185d1a58d8d85b1b081cda1bdd5b190819985a5b60521b60448201526064016102bc565b60408051600481526024810182526020810180516001600160e01b031662dc4c0f60e61b179052905160009182916001600160a01b038716916161a8916103f19190610518565b60006040518083038160008787f1925050503d806000811461042f576040519150601f19603f3d011682016040523d82523d6000602084013e610434565b606091505b5091509150816104795760405162461bcd60e51b815260206004820152601060248201526f63616c6c2073686f756c6420776f726b60801b60448201526064016102bc565b8080602001905181019061048d9190610547565b9550505050505090565b60008060009054906101000a90046001600160a01b03166001600160a01b031663371303c06161a86040518263ffffffff1660e01b81526004016020604051808303818786fa1580156104ee573d6000803e3d6000fd5b50505050506040513d601f19601f820116820180604052508101906105139190610547565b905090565b6000825160005b81811015610539576020818601810151858301520161051f565b506000920191825250919050565b60006020828403121561055957600080fd5b815163ffffffff8116811461056d57600080fd5b939250505056fea2646970667358221220c2f147e85b8c5a1e0a1321dca3f88a6fd4071f78c62a4e7e81c79261ed85480364736f6c63430008140033" + ) + + val NATIVE_CALLER_NESTED_ABI_ID: String = getABIMethodId("testNestedCalls()") + + val NATIVE_CALLER_STATIC_READONLY_ABI_ID: String = getABIMethodId("testStaticCallOnReadonlyMethod()") + + val NATIVE_CALLER_STATIC_READWRITE_ABI_ID: String = getABIMethodId("testStaticCallOnReadwriteMethod()") + + val NATIVE_CALLER_STATIC_RW_CONTRACT_ABI_ID: String = getABIMethodId("testStaticCallOnReadwriteMethodContractCall()") + + //These signatures need to be the same as Storage contract's ones, so it can be used the same caller contract for + // calling them on the Storage contract or on the NativeTestContract. + val NATIVE_CONTRACT_RETRIEVE_ABI_ID: String = STORAGE_RETRIEVE_ABI_ID + + val NATIVE_CONTRACT_INC_ABI_ID: String = STORAGE_INC_ABI_ID + + + // compiled EVM byte-code of SimpleProxy contract + // source: ./qa/SidechainTestFramework/account/smart_contract_resources/contracts/SimpleProxy.sol + // compiled with "solc -bin -o /tmp/simpleProxyOutput SimpleProxy.sol + def simpleProxyContractCode: Array[Byte] = BytesUtils.fromHexString( + "608060405234801561001057600080fd5b50610639806100206000396000f3fe60806040526004361061002d5760003560e01c80631abdc35a146100305780635eec2fe81461006d5761002e565b5b005b34801561003c57600080fd5b506100576004803603810190610052919061033c565b6100aa565b6040516100649190610440565b60405180910390f35b34801561007957600080fd5b50610094600480360381019061008f9190610462565b610174565b6040516100a19190610440565b60405180910390f35b606060005a90506000808773ffffffffffffffffffffffffffffffffffffffff1687849088886040516100de929190610501565b600060405180830381858888f193505050503d806000811461011c576040519150601f19603f3d011682016040523d82523d6000602084013e610121565b606091505b509150915081610166576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161015d90610577565b60405180910390fd5b809350505050949350505050565b606060005a90506000808673ffffffffffffffffffffffffffffffffffffffff168387876040516101a6929190610501565b6000604051808303818686fa925050503d80600081146101e2576040519150601f19603f3d011682016040523d82523d6000602084013e6101e7565b606091505b50915091508161022c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610223906105e3565b60405180910390fd5b8093505050509392505050565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061026e82610243565b9050919050565b61027e81610263565b811461028957600080fd5b50565b60008135905061029b81610275565b92915050565b6000819050919050565b6102b4816102a1565b81146102bf57600080fd5b50565b6000813590506102d1816102ab565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f8401126102fc576102fb6102d7565b5b8235905067ffffffffffffffff811115610319576103186102dc565b5b602083019150836001820283011115610335576103346102e1565b5b9250929050565b6000806000806060858703121561035657610355610239565b5b60006103648782880161028c565b9450506020610375878288016102c2565b935050604085013567ffffffffffffffff8111156103965761039561023e565b5b6103a2878288016102e6565b925092505092959194509250565b600081519050919050565b600082825260208201905092915050565b60005b838110156103ea5780820151818401526020810190506103cf565b60008484015250505050565b6000601f19601f8301169050919050565b6000610412826103b0565b61041c81856103bb565b935061042c8185602086016103cc565b610435816103f6565b840191505092915050565b6000602082019050818103600083015261045a8184610407565b905092915050565b60008060006040848603121561047b5761047a610239565b5b60006104898682870161028c565b935050602084013567ffffffffffffffff8111156104aa576104a961023e565b5b6104b6868287016102e6565b92509250509250925092565b600081905092915050565b82818337600083830152505050565b60006104e883856104c2565b93506104f58385846104cd565b82840190509392505050565b600061050e8284866104dc565b91508190509392505050565b600082825260208201905092915050565b7f63616c6c2073686f756c6420776f726b00000000000000000000000000000000600082015250565b600061056160108361051a565b915061056c8261052b565b602082019050919050565b6000602082019050818103600083015261059081610554565b9050919050565b7f73746174696363616c6c2073686f756c6420776f726b00000000000000000000600082015250565b60006105cd60168361051a565b91506105d882610597565b602082019050919050565b600060208201905081810360008301526105fc816105c0565b905091905056fea26469706673582212208ebd17c968cf2a9ce2a84a89f96b164dda19036f884426b031a31220d82d6cbc64736f6c63430008120033" + ) + + val SIMPLE_PROXY_CALL_ABI_ID: String = getABIMethodId("doCall(address,uint256,bytes)") +} diff --git a/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala new file mode 100644 index 0000000000..d031fb3b2f --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ContractInteropTransferTest.scala @@ -0,0 +1,100 @@ +package io.horizen.account.state + +import io.horizen.evm._ +import io.horizen.utils.BytesUtils +import org.junit.Assert.assertEquals +import org.junit.Test +import sparkz.crypto.hash.Keccak256 + +import java.math.BigInteger + +class ContractInteropTransferTest extends ContractInteropTestBase { + override val processorToTest: NativeSmartContractMsgProcessor = NativeTestContract + + private object NativeTestContract extends NativeSmartContractMsgProcessor { + override val contractAddress: Address = new Address("0x00000000000000000000000000000000deadbeef") + override val contractCode: Array[Byte] = Keccak256.hash("NativeTestContract") + + override def process( + invocation: Invocation, + view: BaseAccountStateView, + context: ExecutionContext + ): Array[Byte] = { + // accept incoming value transfers + if (invocation.value.signum() > 0) { + view.subBalance(invocation.caller, invocation.value) + view.addBalance(contractAddress, invocation.value) + } + // forward value to another account if an address is given + if (invocation.input.length == 20) { + // read target contract address from input + val evmContractAddress = new Address(invocation.input) + // execute nested call to EVM contract + context.execute(invocation.call(evmContractAddress, invocation.value, Array.emptyByteArray, 10000)) + } + Array.emptyByteArray + } + } + + val transferValue: BigInteger = BigInteger.valueOf(100000) + + @Test + def testTransferToNativeContract(): Unit = { + // transfer some value to the native contract directly: EOA => native contract + transition(getMessage(NativeTestContract.contractAddress, value = transferValue)) + assertEquals(initialBalance.subtract(transferValue), stateView.getBalance(origin)) + assertEquals(transferValue, stateView.getBalance(NativeTestContract.contractAddress)) + } + + @Test + def testTransferToEvmContract(): Unit = { + // deploy an EVM-based contract + val address = deploy(ContractInteropTestBase.nativeInteropContractCode) + // transfer some value to the EVM contract directly: EOA => EVM contract + transition(getMessage(address, value = transferValue)) + assertEquals(initialBalance.subtract(transferValue), stateView.getBalance(origin)) + assertEquals(transferValue, stateView.getBalance(address)) + } + + @Test + def testTransferFromNativeToEvmContract(): Unit = { + // deploy an EVM-based contract + val address = deploy(ContractInteropTestBase.nativeInteropContractCode) + // call the native contract with some value and make it forward the amount to the EVM contract + transition(getMessage(NativeTestContract.contractAddress, value = transferValue, data = address.toBytes)) + // origin lost the transferred amount + assertEquals(initialBalance.subtract(transferValue), stateView.getBalance(origin)) + // the native contract does not have any balance, everything should have been forwarded + assertEquals(BigInteger.ZERO, stateView.getBalance(NativeTestContract.contractAddress)) + // the transferred amount reached the EVM contract + assertEquals(transferValue, stateView.getBalance(address)) + } + + @Test + def testTransferFromEvmToNativeContract(): Unit = { + // deploy an EVM-based contract + val address = deploy(ContractInteropTestBase.nativeInteropContractCode) + // call the EVM contract with some value and make it forward the amount to the native contract + val calldata = BytesUtils.fromHexString("7d286e48") ++ new Array[Byte](12) ++ NativeTestContract.contractAddress.toBytes + transition(getMessage(address, value = transferValue, data = calldata)) + // origin lost the transferred amount + assertEquals(initialBalance.subtract(transferValue), stateView.getBalance(origin)) + // the EVM contract does not have any balance, everything should have been forwarded + assertEquals(BigInteger.ZERO, stateView.getBalance(address)) + // the transferred amount reached the native contract + assertEquals(transferValue, stateView.getBalance(NativeTestContract.contractAddress)) + } + + @Test + def testTransferFromNativeToEoa(): Unit = { + // call the native contract with some value and make it forward the amount to the EOA + transition(getMessage(NativeTestContract.contractAddress, value = transferValue, data = origin2.toBytes)) + // origin lost the transferred amount + assertEquals(initialBalance.subtract(transferValue), stateView.getBalance(origin)) + // the native contract does not have any balance, everything should have been forwarded + assertEquals(BigInteger.ZERO, stateView.getBalance(NativeTestContract.contractAddress)) + // the transferred amount reached the EVM contract + assertEquals(transferValue, stateView.getBalance(origin2)) + } +} + diff --git a/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorIntegrationTest.scala b/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorIntegrationTest.scala index 7c94c23ba5..52c688d3a4 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorIntegrationTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorIntegrationTest.scala @@ -1,7 +1,6 @@ package io.horizen.account.state import io.horizen.fixtures.SecretFixture -import io.horizen.utils.ClosableResourceHandler import org.junit.Assert.{assertArrayEquals, assertEquals, assertFalse, assertTrue} import org.junit.Test import org.scalatestplus.junit.JUnitSuite @@ -12,10 +11,10 @@ import java.math.BigInteger import java.nio.charset.StandardCharsets class EoaMessageProcessorIntegrationTest - extends JUnitSuite - with MockitoSugar - with SecretFixture - with MessageProcessorFixture { + extends JUnitSuite + with MockitoSugar + with SecretFixture + with MessageProcessorFixture { @Test def canProcess(): Unit = { @@ -25,22 +24,34 @@ class EoaMessageProcessorIntegrationTest usingView(EoaMessageProcessor) { view => // Test 1: to account doesn't exist, so considered as EOA - assertTrue("Processor expected to BE ABLE to process message", EoaMessageProcessor.canProcess(msg, view, view.getConsensusEpochNumberAsInt)) + assertTrue( + "Processor expected to BE ABLE to process message", + TestContext.canProcess(EoaMessageProcessor, msg, view, view.getConsensusEpochNumberAsInt) + ) // Test 2: to account exists and has NO code hash defined, so considered as EOA // declare account with some coins view.addBalance(toAddress, BigInteger.ONE) - assertTrue("Processor expected to BE ABLE to process message", EoaMessageProcessor.canProcess(msg, view, view.getConsensusEpochNumberAsInt)) + assertTrue( + "Processor expected to BE ABLE to process message", + TestContext.canProcess(EoaMessageProcessor, msg, view, view.getConsensusEpochNumberAsInt) + ) // Test 3: to account exists and has code hash defined, so considered as Smart contract account val codeHash: Array[Byte] = Keccak256.hash("abcd".getBytes(StandardCharsets.UTF_8)) view.addAccount(toAddress, codeHash) - assertFalse("Processor expected to UNABLE to process message", EoaMessageProcessor.canProcess(msg, view, view.getConsensusEpochNumberAsInt)) + assertFalse( + "Processor expected to UNABLE to process message", + TestContext.canProcess(EoaMessageProcessor, msg, view, view.getConsensusEpochNumberAsInt) + ) // Test 4: "to" is null -> smart contract declaration case val data: Array[Byte] = new Array[Byte](100) val contractDeclarationMessage = getMessage(toAddress, value, data) - assertFalse("Processor expected to UNABLE to process message", EoaMessageProcessor.canProcess(contractDeclarationMessage, view, 0)) + assertFalse( + "Processor expected to UNABLE to process message", + TestContext.canProcess(EoaMessageProcessor, contractDeclarationMessage, view, 0) + ) } } diff --git a/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala index d161839170..3946f41dc7 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EoaMessageProcessorTest.scala @@ -1,7 +1,7 @@ package io.horizen.account.state -import io.horizen.fixtures.SecretFixture import io.horizen.evm.Address +import io.horizen.fixtures.SecretFixture import org.junit.Assert.{assertArrayEquals, assertEquals, assertFalse, assertTrue} import org.junit.Test import org.mockito.{ArgumentMatchers, Mockito} @@ -41,14 +41,16 @@ class EoaMessageProcessorTest extends JUnitSuite with MockitoSugar with SecretFi }) assertTrue( "Message for EoaMessageProcessor cannot be processed", - EoaMessageProcessor.canProcess(msg, mockStateView, 0)) + TestContext.canProcess(EoaMessageProcessor, msg, mockStateView, 0) + ) // Test 2: send to EOA account, tx with no-empty "data" val data = new Array[Byte](1000) val msgWithData = getMessage(address, value, data) assertTrue( "Message for EoaMessageProcessor cannot be processed", - EoaMessageProcessor.canProcess(msgWithData, mockStateView, 0)) + TestContext.canProcess(EoaMessageProcessor, msgWithData, mockStateView, 0) + ) // Test 3: Failure: send to smart contract account Mockito.reset(mockStateView) @@ -60,21 +62,24 @@ class EoaMessageProcessorTest extends JUnitSuite with MockitoSugar with SecretFi }) assertFalse( "Message for EoaMessageProcessor wrongly can be processed", - EoaMessageProcessor.canProcess(msg, mockStateView, 0)) + TestContext.canProcess(EoaMessageProcessor, msg, mockStateView, 0) + ) // Test 4: Failure: to is null Mockito.reset(mockStateView) val contractDeclarationMessage = getMessage(null, value, data) assertFalse( "Message for EoaMessageProcessor wrongly can be processed", - EoaMessageProcessor.canProcess(contractDeclarationMessage, mockStateView, 0)) + TestContext.canProcess(EoaMessageProcessor, contractDeclarationMessage, mockStateView, 0) + ) // Test 4: Failure: data is empty array Mockito.reset(mockStateView) val contractDeclarationMessage2 = getMessage(null, value, Array.emptyByteArray) assertFalse( "Message for EoaMessageProcessor wrongly can be processed", - EoaMessageProcessor.canProcess(contractDeclarationMessage2, mockStateView, 0)) + TestContext.canProcess(EoaMessageProcessor, contractDeclarationMessage2, mockStateView, 0) + ) } @Test @@ -105,13 +110,17 @@ class EoaMessageProcessorTest extends JUnitSuite with MockitoSugar with SecretFi Mockito .when(mockStateView.subBalance(ArgumentMatchers.any[Address], ArgumentMatchers.any[BigInteger])) .thenThrow(new ExecutionFailedException("something went error")) - assertThrows[ExecutionFailedException](withGas(EoaMessageProcessor.process(msg, mockStateView, _, defaultBlockContext))) + assertThrows[ExecutionFailedException]( + withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _)) + ) // Test 3: Failure during addBalance Mockito.reset(mockStateView) Mockito .when(mockStateView.addBalance(ArgumentMatchers.any[Address], ArgumentMatchers.any[BigInteger])) .thenThrow(new ExecutionFailedException("something went error")) - assertThrows[ExecutionFailedException](withGas(EoaMessageProcessor.process(msg, mockStateView, _, defaultBlockContext))) + assertThrows[ExecutionFailedException]( + withGas(TestContext.process(EoaMessageProcessor, msg, mockStateView, defaultBlockContext, _)) + ) } } diff --git a/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala b/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala index 4f8bb22c38..711aa313cf 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorIntegrationTest.scala @@ -12,7 +12,8 @@ class EvmMessageProcessorIntegrationTest extends EvmMessageProcessorTestBase { // compiled EVM byte-code of the Storage contract, // source: libevm/contracts/Storage.sol val deployCode: Array[Byte] = BytesUtils.fromHexString( - "608060405234801561001057600080fd5b5060405161023638038061023683398101604081905261002f916100f6565b6000819055604051339060008051602061021683398151915290610073906020808252600c908201526b48656c6c6f20576f726c642160a01b604082015260600190565b60405180910390a2336001600160a01b03166000805160206102168339815191526040516100bf906020808252600a908201526948656c6c6f2045564d2160b01b604082015260600190565b60405180910390a26040517ffe1a3ad11e425db4b8e6af35d11c50118826a496df73006fc724cb27f2b9994690600090a15061010f565b60006020828403121561010857600080fd5b5051919050565b60f98061011d6000396000f3fe60806040526004361060305760003560e01c80632e64cec1146035578063371303c01460565780636057361d14606a575b600080fd5b348015604057600080fd5b5060005460405190815260200160405180910390f35b348015606157600080fd5b506068607a565b005b606860753660046086565b600055565b6000546075906001609e565b600060208284031215609757600080fd5b5035919050565b6000821982111560be57634e487b7160e01b600052601160045260246000fd5b50019056fea264697066735822122080d9db531d29b1bd6b4e16762726b70e2a94f0b40ee4e2ab534d9b879cf1c25664736f6c634300080f00330738f4da267a110d810e6e89fc59e46be6de0c37b1d5cd559b267dc3688e74e0") + "608060405234801561001057600080fd5b5060405161023638038061023683398101604081905261002f916100f6565b6000819055604051339060008051602061021683398151915290610073906020808252600c908201526b48656c6c6f20576f726c642160a01b604082015260600190565b60405180910390a2336001600160a01b03166000805160206102168339815191526040516100bf906020808252600a908201526948656c6c6f2045564d2160b01b604082015260600190565b60405180910390a26040517ffe1a3ad11e425db4b8e6af35d11c50118826a496df73006fc724cb27f2b9994690600090a15061010f565b60006020828403121561010857600080fd5b5051919050565b60f98061011d6000396000f3fe60806040526004361060305760003560e01c80632e64cec1146035578063371303c01460565780636057361d14606a575b600080fd5b348015604057600080fd5b5060005460405190815260200160405180910390f35b348015606157600080fd5b506068607a565b005b606860753660046086565b600055565b6000546075906001609e565b600060208284031215609757600080fd5b5035919050565b6000821982111560be57634e487b7160e01b600052601160045260246000fd5b50019056fea264697066735822122080d9db531d29b1bd6b4e16762726b70e2a94f0b40ee4e2ab534d9b879cf1c25664736f6c634300080f00330738f4da267a110d810e6e89fc59e46be6de0c37b1d5cd559b267dc3688e74e0" + ) @Test def testCanProcess(): Unit = { @@ -23,16 +24,22 @@ class EvmMessageProcessorIntegrationTest extends EvmMessageProcessorTestBase { stateView.addBalance(eoaAddress, BigInteger.TEN) val processor = new EvmMessageProcessor() - assertTrue("should process smart contract deployment", processor.canProcess(getMessage(null), stateView, 0)) + assertTrue( + "should process smart contract deployment", + TestContext.canProcess(processor, getMessage(null), stateView, 0) + ) assertTrue( "should process calls to existing smart contracts", - processor.canProcess(getMessage(contractAddress), stateView, 0)) + TestContext.canProcess(processor, getMessage(contractAddress), stateView, 0) + ) assertFalse( "should not process EOA to EOA transfer (empty account)", - processor.canProcess(getMessage(emptyAddress), stateView, 0)) + TestContext.canProcess(processor, getMessage(emptyAddress), stateView, 0) + ) assertFalse( "should not process EOA to EOA transfer (non-empty account)", - processor.canProcess(getMessage(eoaAddress), stateView, 0)) + TestContext.canProcess(processor, getMessage(eoaAddress), stateView, 0) + ) } } diff --git a/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorTest.scala index c372c10a5c..4bd86a60aa 100644 --- a/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/EvmMessageProcessorTest.scala @@ -5,8 +5,8 @@ import org.junit.Assert.{assertFalse, assertNotNull, assertTrue} import org.junit.Test import org.mockito.{ArgumentMatchers, Mockito} import org.scalatestplus.mockito.MockitoSugar -import java.nio.charset.StandardCharsets +import java.nio.charset.StandardCharsets class EvmMessageProcessorTest extends EvmMessageProcessorTestBase with MockitoSugar { @Test @@ -30,18 +30,30 @@ class EvmMessageProcessorTest extends EvmMessageProcessorTestBase with MockitoSu contractAddress.equals(address) }) - assertTrue("should process smart contract deployment", processor.canProcess(getMessage(null), mockStateView, 0)) + assertTrue( + "should process smart contract deployment", + TestContext.canProcess(processor, getMessage(null), mockStateView, 0) + ) assertTrue( "should process calls to existing smart contracts", - processor.canProcess(getMessage(contractAddress), mockStateView, 0)) + TestContext.canProcess(processor, getMessage(contractAddress), mockStateView, 0) + ) assertFalse( "should not process EOA to EOA transfer (empty account)", - processor.canProcess(getMessage(emptyAddress), mockStateView, 0)) + TestContext.canProcess(processor, getMessage(emptyAddress), mockStateView, 0) + ) assertFalse( "should not process EOA to EOA transfer (non-empty account)", - processor.canProcess(getMessage(eoaAddress), mockStateView, 0)) + TestContext.canProcess(processor, getMessage(eoaAddress), mockStateView, 0) + ) assertFalse( "should ignore data on EOA to EOA transfer", - processor.canProcess(getMessage(eoaAddress, data = "the same thing we do every night, pinky".getBytes(StandardCharsets.UTF_8)), mockStateView, 0)) + TestContext.canProcess( + processor, + getMessage(eoaAddress, data = "the same thing we do every night, pinky".getBytes(StandardCharsets.UTF_8)), + mockStateView, + 0 + ) + ) } } diff --git a/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala index 8b1b2be4d4..26d034ce50 100644 --- a/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/ForgerStakeMsgProcessorTest.scala @@ -7,12 +7,12 @@ import io.horizen.account.state.ForgerStakeMsgProcessor.{AddNewStakeCmd, GetList import io.horizen.account.state.events.{DelegateForgerStake, OpenForgerList, WithdrawForgerStake} import io.horizen.account.state.receipt.EthereumConsensusDataLog import io.horizen.account.utils.{EthereumTransactionDecoder, ZenWeiConverter} +import io.horizen.evm.Address import io.horizen.fixtures.StoreFixture import io.horizen.params.NetworkParams import io.horizen.proposition.{PublicKey25519Proposition, VrfPublicKey} import io.horizen.secret.PrivateKey25519 import io.horizen.utils.{BytesUtils, Ed25519} -import io.horizen.evm.Address import org.junit.Assert._ import org.junit._ import org.mockito._ @@ -93,7 +93,7 @@ class ForgerStakeMsgProcessorTest val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(RemoveStakeCmd) ++ data, nonce) // try processing the removal of stake, should succeed - val returnData = withGas(forgerStakeMessageProcessor.process(msg, stateView, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, _)) assertNotNull(returnData) assertArrayEquals(stakeId, returnData) } @@ -101,7 +101,7 @@ class ForgerStakeMsgProcessorTest def getForgerStakeList(stateView: AccountStateView): Array[Byte] = { val msg = getMessage(contractAddress, 0, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) val (returnData, usedGas) = withGas { gas => - val result = forgerStakeMessageProcessor.process(msg, stateView, gas, defaultBlockContext) + val result = TestContext.process(forgerStakeMessageProcessor, msg, stateView, defaultBlockContext, gas) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -117,6 +117,9 @@ class ForgerStakeMsgProcessorTest assertEquals("Wrong MethodId for GetListOfForgersCmd", "f6ad3c23", ForgerStakeMsgProcessor.GetListOfForgersCmd) assertEquals("Wrong MethodId for AddNewStakeCmd", "5ca748ff", ForgerStakeMsgProcessor.AddNewStakeCmd) assertEquals("Wrong MethodId for RemoveStakeCmd", "f7419d79", ForgerStakeMsgProcessor.RemoveStakeCmd) + //TODO OpenStakeForgerListCmd signature is wrong, it misses a closing parenthesis. Fixing it requires an hard fork so + // for the moment we stick with the wrong one. + assertEquals("Wrong MethodId for OpenStakeForgerListCmd", "b05bf06c", ForgerStakeMsgProcessor.OpenStakeForgerListCmd) } @Test @@ -138,11 +141,11 @@ class ForgerStakeMsgProcessorTest forgerStakeMessageProcessor.init(view, view.getConsensusEpochNumberAsInt) // correct contract address - assertTrue(forgerStakeMessageProcessor.canProcess(getMessage(forgerStakeMessageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + assertTrue(TestContext.canProcess(forgerStakeMessageProcessor, getMessage(forgerStakeMessageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) // wrong address - assertFalse(forgerStakeMessageProcessor.canProcess(getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(forgerStakeMessageProcessor, getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) // contact deployment: to == null - assertFalse(forgerStakeMessageProcessor.canProcess(getMessage(null), view, view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(forgerStakeMessageProcessor, getMessage(null), view, view.getConsensusEpochNumberAsInt)) view.commit(bytesToVersion(getVersion.data())) } @@ -430,7 +433,9 @@ class ForgerStakeMsgProcessorTest val txHash2 = Keccak256.hash("second tx") view.setupTxContext(txHash2, 10) // try processing a msg with the same stake (same msg), should fail - assertThrows[ExecutionRevertedException](withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext))) + assertThrows[ExecutionRevertedException]( + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) + ) // Checking that log doesn't change listOfLogs = view.getLogs(txHash2) @@ -442,7 +447,7 @@ class ForgerStakeMsgProcessorTest // should fail because input has a trailing byte val ex = intercept[ExecutionRevertedException] { - withGas(forgerStakeMessageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(forgerStakeMessageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -746,7 +751,7 @@ class ForgerStakeMsgProcessorTest ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount))) - val returnData = withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -798,7 +803,7 @@ class ForgerStakeMsgProcessorTest listOfExpectedForgerStakes.add(AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount))) - val returnData = withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -855,7 +860,9 @@ class ForgerStakeMsgProcessorTest val addNewStakeMsg = getMessage(contractAddress, stakeAmount, BytesUtils.fromHexString(AddNewStakeCmd) ++ addNewStakeData, randomNonce) val expStakeId = forgerStakeMessageProcessor.getStakeId(addNewStakeMsg) val forgingStakeInfo = AccountForgingStakeInfo(expStakeId, ForgerStakeData(ForgerPublicKeys(blockSignerProposition, vrfPublicKey), ownerAddressProposition, stakeAmount)) - val addNewStakeReturnData = withGas(forgerStakeMessageProcessor.process(addNewStakeMsg, view, _, defaultBlockContext)) + val addNewStakeReturnData = withGas( + TestContext.process(forgerStakeMessageProcessor, addNewStakeMsg, view, defaultBlockContext, _) + ) assertNotNull(addNewStakeReturnData) val nonce = randomNonce @@ -870,20 +877,20 @@ class ForgerStakeMsgProcessorTest // should fail because value in msg should be 0 (value=1) var msg = getMessage(contractAddress, BigInteger.ONE, BytesUtils.fromHexString(RemoveStakeCmd) ++ data, nonce) assertThrows[ExecutionRevertedException] { - withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) } // should fail because value in msg should be 0 (value=-1) msg = getMessage(contractAddress, BigInteger.valueOf(-1), BytesUtils.fromHexString(RemoveStakeCmd) ++ data, nonce) assertThrows[ExecutionRevertedException] { - withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) } // should fail because input data has a trailing byte val badData = Bytes.concat(data, new Array[Byte](1)) msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RemoveStakeCmd) ++ badData, nonce) val ex = intercept[ExecutionRevertedException] { - withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -909,7 +916,7 @@ class ForgerStakeMsgProcessorTest msg = getMessage(contractAddress, BigInteger.ZERO, BytesUtils.fromHexString(RemoveStakeCmd) ++ badData2, nonce) val ex2 = intercept[ExecutionRevertedException] { - withGas(forgerStakeMessageProcessor.process(msg, view, _, defaultBlockContext)) + withGas(TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, _)) } assertTrue(ex2.getMessage.contains("ill-formed signature")) @@ -949,13 +956,13 @@ class ForgerStakeMsgProcessorTest var msg = getMessage(contractAddress, BigInteger.ONE, BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - forgerStakeMessageProcessor.process(msg, view, gas, defaultBlockContext) + TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas) } msg = getMessage(contractAddress, BigInteger.valueOf(-1), BytesUtils.fromHexString(GetListOfForgersCmd), randomNonce) assertThrows[ExecutionRevertedException] { - forgerStakeMessageProcessor.process(msg, view, gas, defaultBlockContext) + TestContext.process(forgerStakeMessageProcessor, msg, view, defaultBlockContext, gas) } } } diff --git a/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala index 3a152bb803..3ee465b3af 100644 --- a/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/McAddrOwnershipMsgProcessorTest.scala @@ -150,16 +150,16 @@ class McAddrOwnershipMsgProcessorTest usingView(messageProcessor) { view => - assertTrue(McAddrOwnershipMsgProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) messageProcessor.init(view, view.getConsensusEpochNumberAsInt) assertTrue(view.accountExists(contractAddress)) assertFalse(view.isEoaAccount(contractAddress)) assertTrue(view.isSmartContractAccount(contractAddress)) - assertTrue(McAddrOwnershipMsgProcessor.initDone(view)) + assertTrue(messageProcessor.initDone(view)) view.commit(bytesToVersion(getVersion.data())) } @@ -175,15 +175,15 @@ class McAddrOwnershipMsgProcessorTest usingView(messageProcessor) { view => assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) - assertFalse(McAddrOwnershipMsgProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) messageProcessor.init(view, view.getConsensusEpochNumberAsInt) // assert no initialization took place assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) } } @@ -193,15 +193,15 @@ class McAddrOwnershipMsgProcessorTest usingView(messageProcessor) { view => - assertTrue(McAddrOwnershipMsgProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) messageProcessor.init(view, view.getConsensusEpochNumberAsInt) assertTrue(view.accountExists(contractAddress)) - assertTrue(McAddrOwnershipMsgProcessor.initDone(view)) + assertTrue(messageProcessor.initDone(view)) view.commit(bytesToVersion(getVersion.data())) @@ -219,12 +219,12 @@ class McAddrOwnershipMsgProcessorTest // assert no initialization took place yet assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) - assertTrue(McAddrOwnershipMsgProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) // correct contract address - assertTrue(messageProcessor.canProcess(getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + assertTrue(TestContext.canProcess(messageProcessor, getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) // check initialization took place assertTrue(view.accountExists(contractAddress)) @@ -232,12 +232,12 @@ class McAddrOwnershipMsgProcessorTest assertFalse(view.isEoaAccount(contractAddress)) // call a second time for checking it does not do init twice (would assert) - assertTrue(messageProcessor.canProcess(getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + assertTrue(TestContext.canProcess(messageProcessor, getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) // wrong address - assertFalse(messageProcessor.canProcess(getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(messageProcessor, getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) // contract deployment: to == null - assertFalse(messageProcessor.canProcess(getMessage(null), view, view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(messageProcessor, getMessage(null), view, view.getConsensusEpochNumberAsInt)) view.commit(bytesToVersion(getVersion.data())) } @@ -266,14 +266,14 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1 ) - assertFalse(McAddrOwnershipMsgProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) // correct contract address and message but fork not yet reached - assertFalse(messageProcessor.canProcess(msg, view, view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(messageProcessor, msg, view, view.getConsensusEpochNumberAsInt)) // the init did not take place assertFalse(view.accountExists(contractAddress)) - assertFalse(McAddrOwnershipMsgProcessor.initDone(view)) + assertFalse(messageProcessor.initDone(view)) view.commit(bytesToVersion(getVersion.data())) } @@ -322,7 +322,7 @@ class McAddrOwnershipMsgProcessorTest val txHash2 = Keccak256.hash("second tx") view.setupTxContext(txHash2, 10) // try processing a msg with the same data (same msg), should fail - assertThrows[ExecutionRevertedException](withGas(messageProcessor.process(msg, view, _, defaultBlockContext))) + assertThrows[ExecutionRevertedException](withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _))) // Checking that log doesn't change listOfLogs = view.getLogs(txHash2) @@ -482,7 +482,7 @@ class McAddrOwnershipMsgProcessorTest listOfAllExpectedData.add(McAddrOwnershipData(scAddrStr1.toLowerCase(), mcAddr)) listOfScAddress1ExpectedData.add(McAddrOwnershipData(scAddrStr1.toLowerCase(), mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -502,7 +502,7 @@ class McAddrOwnershipMsgProcessorTest listOfAllExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) listOfScAddress2ExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -573,7 +573,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -639,7 +639,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -653,7 +653,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -666,7 +666,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("already associated")) @@ -679,7 +679,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -694,7 +694,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -709,7 +709,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Invalid mc signature")) @@ -738,7 +738,7 @@ class McAddrOwnershipMsgProcessorTest ) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msg2, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains(s"already associated to sc address ${scAddrStr1.toLowerCase()}")) } @@ -777,7 +777,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) var ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -787,7 +787,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Value must be zero")) @@ -798,7 +798,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, scAddressObj1 ) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("Wrong message data field length")) @@ -808,7 +808,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } assertTrue(ex.getMessage.contains("account does not exist")) @@ -819,7 +819,7 @@ class McAddrOwnershipMsgProcessorTest randomNonce, origin ) ex = intercept[ExecutionRevertedException] { - withGas(messageProcessor.process(msgBad, view, _, defaultBlockContext)) + withGas(TestContext.process(messageProcessor, msgBad, view, defaultBlockContext, _)) } val ownershipIdStr = BytesUtils.toHexString(getOwnershipId(mcAddrStr1)) assertTrue(ex.getMessage.contains("is not the owner")) @@ -902,7 +902,7 @@ class McAddrOwnershipMsgProcessorTest ) // try processing the removal of ownership, should succeed - val returnData = withGas(messageProcessor.process(msg, stateView, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, _)) assertNotNull(returnData) assertArrayEquals(getOwnershipId(mcTransparentAddress), returnData) } @@ -912,7 +912,7 @@ class McAddrOwnershipMsgProcessorTest stateView.setupAccessList(msg) val (returnData, usedGas) = withGas { gas => - val result = messageProcessor.process(msg, stateView, gas, defaultBlockContext) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -931,9 +931,8 @@ class McAddrOwnershipMsgProcessorTest contractAddress, 0, BytesUtils.fromHexString(GetListOfOwnershipsCmd) ++ data, randomNonce) stateView.setupAccessList(msg) - val (returnData, usedGas) = withGas { gas => - val result = messageProcessor.process(msg, stateView, gas, defaultBlockContext) + val result = TestContext.process(messageProcessor, msg, stateView, defaultBlockContext, gas) (result, gas.getUsedGas) } // gas consumption depends on the number of items in the list @@ -972,7 +971,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) @@ -994,7 +993,7 @@ class McAddrOwnershipMsgProcessorTest scAddressObj1 ) - val returnData2 = withGas(messageProcessor.process(msg2, view, _, defaultBlockContext)) + val returnData2 = withGas(TestContext.process(messageProcessor, msg2, view, defaultBlockContext, _)) assertNotNull(returnData2) println("This is the returned value: " + BytesUtils.toHexString(returnData2)) @@ -1030,7 +1029,7 @@ class McAddrOwnershipMsgProcessorTest listOfScAddress2ExpectedData.add(McAddrOwnershipData(scAddrStr2.toLowerCase(), mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } @@ -1048,7 +1047,7 @@ class McAddrOwnershipMsgProcessorTest listOfExpectedData.add(McAddrOwnershipData(scAddrStr1, mcAddr)) - val returnData = withGas(messageProcessor.process(msg, view, _, defaultBlockContext)) + val returnData = withGas(TestContext.process(messageProcessor, msg, view, defaultBlockContext, _)) assertNotNull(returnData) } diff --git a/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala b/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala index 8b7ed2982f..e536750460 100644 --- a/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala +++ b/sdk/src/test/scala/io/horizen/account/state/MessageProcessorFixture.scala @@ -23,6 +23,7 @@ trait MessageProcessorFixture extends AccountFixture with ClosableResourceHandle val origin: Address = randomAddress + val origin2: Address = randomAddress val defaultBlockContext = new BlockContext(Address.ZERO, 0, 0, DefaultGasFeeFork.blockGasLimit, 0, 33, 0, 1, MockedHistoryBlockHashProvider, Hash.ZERO) def usingView(processors: Seq[MessageProcessor])(fun: AccountStateView => Unit): Unit = { @@ -84,7 +85,7 @@ trait MessageProcessorFixture extends AccountFixture with ClosableResourceHandle ): Array[Byte] = { view.setupAccessList(msg) val gas = new GasPool(1000000) - val result = Try.apply(processor.process(msg, view, gas, ctx)) + val result = Try.apply(TestContext.process(processor, msg, view, ctx, gas)) assertEquals("Unexpected gas consumption", expectedGas, gas.getUsedGas) // return result or rethrow any exception result.get diff --git a/sdk/src/test/scala/io/horizen/account/state/ProxyMsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/ProxyMsgProcessorTest.scala new file mode 100644 index 0000000000..0acb63612f --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/ProxyMsgProcessorTest.scala @@ -0,0 +1,245 @@ +package io.horizen.account.state + +import io.horizen.account.fork.ContractInteroperabilityFork +import io.horizen.account.fork.GasFeeFork.DefaultGasFeeFork +import io.horizen.consensus.intToConsensusEpochNumber +import io.horizen.evm.Address +import io.horizen.fixtures.StoreFixture +import io.horizen.fork.{ForkConfigurator, ForkManagerUtil, OptionalSidechainFork, SidechainForkConsensusEpoch} +import io.horizen.params.{MainNetParams, NetworkParams, RegTestParams, TestNetParams} +import io.horizen.utils.{BytesUtils, Pair} +import org.junit.Assert.{assertFalse, _} +import org.junit._ +import org.mockito.Mockito +import org.scalatestplus.junit.JUnitSuite +import org.scalatestplus.mockito._ +import sparkz.core.bytesToVersion +import sparkz.crypto.hash.Keccak256 + +import java.math.BigInteger +import java.util +import scala.jdk.CollectionConverters.seqAsJavaListConverter + +class ProxyMsgProcessorTest + extends JUnitSuite + with MockitoSugar + with MessageProcessorFixture + with StoreFixture { + + val MOCK_FORK_POINT: Int = 100 + + class TestOptionalForkConfigurator extends ForkConfigurator { + override val fork1activation: SidechainForkConsensusEpoch = SidechainForkConsensusEpoch(0, 0, 0) + + override def getOptionalSidechainForks: util.List[Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]] = + Seq[Pair[SidechainForkConsensusEpoch, OptionalSidechainFork]]( + new Pair(SidechainForkConsensusEpoch(MOCK_FORK_POINT, MOCK_FORK_POINT, MOCK_FORK_POINT), ContractInteroperabilityFork(true)), + ).asJava + } + + override val defaultBlockContext = new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + MOCK_FORK_POINT, + 0, + 1, + MockedHistoryBlockHashProvider, + new io.horizen.evm.Hash(new Array[Byte](32)) + ) + + @Before + def init(): Unit = { + ForkManagerUtil.initializeForkManager(new TestOptionalForkConfigurator, "regtest") + // by default start with fork active + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn(Option(intToConsensusEpochNumber(MOCK_FORK_POINT))) + } + + val validWeiAmount: BigInteger = new BigInteger("10000000000") + + val mockNetworkParams: NetworkParams = mock[RegTestParams] + val messageProcessor: ProxyMsgProcessor = ProxyMsgProcessor(mockNetworkParams) + val contractAddress: Address = messageProcessor.contractAddress + + val scAddrStr1: String = "00C8F107a09cd4f463AFc2f1E6E5bF6022Ad4600" + val scAddressObj1 = new Address("0x"+scAddrStr1) + + def randomNonce: BigInteger = randomU256 + + @Test + def testMethodIds(): Unit = { + //The expected methodIds were calculated using this site: https://emn178.github.io/online-tools/keccak_256.html + assertEquals("Wrong MethodId for InvokeSmartContractCallCmd", "9b679b4d", ProxyMsgProcessor.InvokeSmartContractCallCmd) + assertEquals("Wrong MethodId for InvokeSmartContractStaticCallCmd", "1c6af61c", ProxyMsgProcessor.InvokeSmartContractStaticCallCmd) + } + + @Test + def testInit(): Unit = { + + usingView(messageProcessor) { view => + + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(messageProcessor.initDone(view)) + + messageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + assertTrue(view.accountExists(contractAddress)) + assertFalse(view.isEoaAccount(contractAddress)) + assertTrue(view.isSmartContractAccount(contractAddress)) + assertTrue(messageProcessor.initDone(view)) + + assertArrayEquals(messageProcessor.contractCodeHash, view.getCodeHash(contractAddress)) + view.commit(bytesToVersion(getVersion.data())) + } + } + + + @Test + def testInitBeforeFork(): Unit = { + + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn( + Option(intToConsensusEpochNumber(MOCK_FORK_POINT - 1))) + + usingView(messageProcessor) { view => + + assertFalse(view.accountExists(contractAddress)) + assertFalse(messageProcessor.initDone(view)) + + assertFalse(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + messageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + // assert no initialization took place + assertFalse(messageProcessor.initDone(view)) + } + } + + @Test + def testInitWithAccountAlreadyExisting(): Unit = { + usingView(messageProcessor) { view => + view.addAccount(contractAddress, Keccak256.hash("whatever")) + view.commit(bytesToVersion(getVersion.data())) + + messageProcessor.init(view, view.getConsensusEpochNumberAsInt) + assertTrue(view.accountExists(contractAddress)) + assertFalse(view.isEoaAccount(contractAddress)) + assertTrue(view.isSmartContractAccount(contractAddress)) + assertTrue(messageProcessor.initDone(view)) + view.commit(bytesToVersion(getVersion.data())) + } + } + + @Test + def testDoubleInit(): Unit = { + + usingView(messageProcessor) { view => + + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(messageProcessor.initDone(view)) + + messageProcessor.init(view, view.getConsensusEpochNumberAsInt) + + assertTrue(messageProcessor.initDone(view)) + + view.commit(bytesToVersion(getVersion.data())) + + val ex = intercept[MessageProcessorInitializationException] { + messageProcessor.init(view, view.getConsensusEpochNumberAsInt) + } + assertTrue(ex.getMessage.contains("already init")) + } + } + + + @Test + def testCanProcessOnTestNetNetwork(): Unit = { + + val messageProcessorOnTestNet: ProxyMsgProcessor = ProxyMsgProcessor(mock[TestNetParams]) + usingView(messageProcessorOnTestNet) { view => + assertTrue(messageProcessorOnTestNet.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(messageProcessorOnTestNet, + getMessage(messageProcessorOnTestNet.contractAddress), view, view.getConsensusEpochNumberAsInt)) + } + } + + @Test + def testCanProcessOnMainNetNetwork(): Unit = { + + val messageProcessorOnMainNet: ProxyMsgProcessor = ProxyMsgProcessor(mock[MainNetParams]) + usingView(messageProcessorOnMainNet) { view => + assertTrue(messageProcessorOnMainNet.isForkActive(view.getConsensusEpochNumberAsInt)) + assertFalse(TestContext.canProcess(messageProcessorOnMainNet, + getMessage(messageProcessorOnMainNet.contractAddress), view, view.getConsensusEpochNumberAsInt)) + } + } + + + @Test + def testCanProcess(): Unit = { + + usingView(messageProcessor) { view => + + // assert no initialization took place yet + assertFalse(messageProcessor.initDone(view)) + + assertTrue(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + // correct contract address + assertTrue(TestContext.canProcess(messageProcessor, getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + + // check initialization took place + assertTrue(view.accountExists(contractAddress)) + assertTrue(view.isSmartContractAccount(contractAddress)) + assertFalse(view.isEoaAccount(contractAddress)) + + // call a second time for checking it does not do init twice (would assert) + assertTrue(TestContext.canProcess(messageProcessor, getMessage(messageProcessor.contractAddress), view, view.getConsensusEpochNumberAsInt)) + + // wrong address + assertFalse(TestContext.canProcess(messageProcessor, getMessage(randomAddress), view, view.getConsensusEpochNumberAsInt)) + // contract deployment: to == null + assertFalse(TestContext.canProcess(messageProcessor, getMessage(null), view, view.getConsensusEpochNumberAsInt)) + + view.commit(bytesToVersion(getVersion.data())) + } + } + + @Test + def testCanNotProcessBeforeFork(): Unit = { + + Mockito.when(metadataStorageView.getConsensusEpochNumber).thenReturn( + Option(intToConsensusEpochNumber(1))) + + usingView(messageProcessor) { view => + + // create sender account with some fund in it + val initialAmount = BigInteger.valueOf(100).multiply(validWeiAmount) + val txHash1 = Keccak256.hash("tx") + view.setupTxContext(txHash1, 10) + createSenderAccount(view, initialAmount, scAddressObj1) + val cmdInput = InvokeSmartContractCmdInput(WithdrawalMsgProcessor.contractAddress, WithdrawalMsgProcessor.GetListOfWithdrawalReqsCmdSig) + val data: Array[Byte] = cmdInput.encode() + val msg = getMessage( + contractAddress, + BigInteger.ZERO, + BytesUtils.fromHexString(ProxyMsgProcessor.InvokeSmartContractStaticCallCmd) ++ data, + randomNonce, + scAddressObj1 + ) + + assertFalse(messageProcessor.isForkActive(view.getConsensusEpochNumberAsInt)) + + // correct contract address and message but fork not yet reached + assertFalse(TestContext.canProcess(messageProcessor, msg, view, view.getConsensusEpochNumberAsInt)) + + // the init did not take place + assertFalse(messageProcessor.initDone(view)) + + view.commit(bytesToVersion(getVersion.data())) + } + } + + +} diff --git a/sdk/src/test/scala/io/horizen/account/state/TestContext.scala b/sdk/src/test/scala/io/horizen/account/state/TestContext.scala new file mode 100644 index 0000000000..da078872ea --- /dev/null +++ b/sdk/src/test/scala/io/horizen/account/state/TestContext.scala @@ -0,0 +1,29 @@ +package io.horizen.account.state + +case class TestContext(msg: Message, blockContext: BlockContext) extends ExecutionContext { + override var depth = 0 + override def execute(invocation: Invocation): Array[Byte] = ??? +} + +object TestContext { + + /** + * Creates a top level invocation from the given message and calls "canProcess" on the message processor. + */ + def canProcess(processor: MessageProcessor, msg: Message, view: BaseAccountStateView, consensusEpochNumber: Int): Boolean = { + processor.canProcess(Invocation.fromMessage(msg), view, consensusEpochNumber) + } + + /** + * Creates a top level invocation from the given message and executes it with the message processor. + */ + def process( + processor: MessageProcessor, + msg: Message, + view: BaseAccountStateView, + blockContext: BlockContext, + gasPool: GasPool + ): Array[Byte] = { + processor.process(Invocation.fromMessage(msg, gasPool), view, TestContext(msg, blockContext)) + } +} diff --git a/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala b/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala index f4abf42d71..bf8322c2fe 100644 --- a/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala +++ b/sdk/src/test/scala/io/horizen/account/state/WithdrawalMsgProcessorTest.scala @@ -21,11 +21,17 @@ import java.util import java.util.Optional import scala.util.Random -class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with WithdrawalMsgProcessorFixture with StoreFixture{ +class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with WithdrawalMsgProcessorFixture + with StoreFixture { var mockStateView: AccountStateView = _ - def getDefaultMessage(opCode: Array[Byte], arguments: Array[Byte], nonce: BigInteger, value: BigInteger = 0): Message = { + def getDefaultMessage( + opCode: Array[Byte], + arguments: Array[Byte], + nonce: BigInteger, + value: BigInteger = 0 + ): Message = { val data = Bytes.concat(opCode, arguments) new Message( origin, @@ -37,7 +43,8 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd value, nonce, data, - false) + false + ) } def randomNonce: BigInteger = randomU256 @@ -81,13 +88,13 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd val msg = addWithdrawalRequestMessage(BigInteger.ONE) assertTrue( "Message for WithdrawalMsgProcessor cannot be processed", - WithdrawalMsgProcessor.canProcess(msg, mockStateView, 0) + TestContext.canProcess(WithdrawalMsgProcessor, msg, mockStateView, 0) ) val wrongAddress = new Address("0x35fdd51e73221f467b40946c97791a3e19799bea") val msgNotProcessable = getMessage(wrongAddress, BigInteger.ZERO, Array.emptyByteArray) assertFalse( "Message not for WithdrawalMsgProcessor can be processed", - WithdrawalMsgProcessor.canProcess(msgNotProcessable, mockStateView, 0) + TestContext.canProcess(WithdrawalMsgProcessor, msgNotProcessable, mockStateView, 0) ) } @@ -99,11 +106,12 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd val data = BytesUtils.fromHexString("99") val msgWithWrongFunctionCall = getMessage(WithdrawalMsgProcessor.contractAddress, value, data) assertThrows[ExecutionRevertedException] { - withGas(WithdrawalMsgProcessor.process(msgWithWrongFunctionCall, mockStateView, _, defaultBlockContext)) + withGas( + TestContext.process(WithdrawalMsgProcessor, msgWithWrongFunctionCall, mockStateView, defaultBlockContext, _) + ) } } - @Test def testProcessShortOpCode(): Unit = { usingView(WithdrawalMsgProcessor) { view => @@ -139,7 +147,6 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd } } - @Test def testAddWithdrawalRequestFailures(): Unit = { Mockito @@ -150,7 +157,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd val withdrawalAmount = ZenWeiConverter.convertZenniesToWei(50) val msg = getMessage(WithdrawalMsgProcessor.contractAddress, withdrawalAmount, Array.emptyByteArray) assertThrows[ExecutionRevertedException]( - withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) ) // helper: mock balance call and assert that the withdrawal request throws @@ -158,7 +165,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd val msg = addWithdrawalRequestMessage(withdrawalAmount) Mockito.when(mockStateView.getBalance(msg.getFrom)).thenReturn(balance) assertThrows[ExecutionRevertedException]( - withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, blockContext)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, blockContext, _)) ) } @@ -171,7 +178,18 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd // Withdrawal request processing when max number of wt was already reached should result in ExecutionFailed val epochNum = 102 val testEpochBlockContext = - new BlockContext(Address.ZERO, 0, 0, DefaultGasFeeFork.blockGasLimit, 0, 0, epochNum, 1, MockedHistoryBlockHashProvider, Hash.ZERO) + new BlockContext( + Address.ZERO, + 0, + 0, + DefaultGasFeeFork.blockGasLimit, + 0, + 0, + epochNum, + 1, + MockedHistoryBlockHashProvider, + Hash.ZERO + ) val key = WithdrawalMsgProcessor.getWithdrawalEpochCounterKey(epochNum) val numOfWithdrawalReqs = Bytes.concat( new Array[Byte](32 - Ints.BYTES), @@ -193,7 +211,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd // Withdrawal request list with invalid data should throw ExecutionRevertedException assertThrows[ExecutionRevertedException]( - withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) ) // No withdrawal requests @@ -205,7 +223,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd .when(mockStateView.getAccountStorage(WithdrawalMsgProcessor.contractAddress, counterKey)) .thenReturn(numOfWithdrawalReqs) - var returnData = withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext)) + var returnData = withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) val expectedListOfWR = new util.ArrayList[WithdrawalRequest]() assertArrayEquals(WithdrawalRequestsListEncoder.encode(expectedListOfWR), returnData) @@ -234,7 +252,8 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd mockWithdrawalRequestsList.get(new ByteArrayWrapper(key)) }) - returnData = withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext), 10000000) + returnData = + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _), 10000000) assertArrayEquals(WithdrawalRequestsListEncoder.encode(expectedListOfWR), returnData) } @@ -247,7 +266,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd ) assertThrows[ExecutionRevertedException] { - withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) } msg = getMessage( @@ -257,7 +276,7 @@ class WithdrawalMsgProcessorTest extends JUnitSuite with MockitoSugar with Withd ) assertThrows[ExecutionRevertedException] { - withGas(WithdrawalMsgProcessor.process(msg, mockStateView, _, defaultBlockContext)) + withGas(TestContext.process(WithdrawalMsgProcessor, msg, mockStateView, defaultBlockContext, _)) } } } diff --git a/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala b/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala index fd9383d4ae..08c75a6e3d 100644 --- a/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala +++ b/sdk/src/test/scala/io/horizen/account/storage/AccountStateMetadataStorageViewTest.scala @@ -8,14 +8,18 @@ import io.horizen.account.utils.AccountBlockFeeInfo import io.horizen.block.{WithdrawalEpochCertificate, WithdrawalEpochCertificateFixture} import io.horizen.consensus.{ConsensusEpochNumber, intToConsensusEpochNumber} import io.horizen.fixtures.{SecretFixture, StoreFixture, TransactionFixture} -import io.horizen.utils.{BytesUtils, WithdrawalEpochInfo} +import io.horizen.storage.Storage +import io.horizen.utils.{ByteArrayWrapper, BytesUtils, WithdrawalEpochInfo} import org.junit.Assert._ import org.junit._ +import org.mockito.Mockito.when import org.scalatestplus.junit.JUnitSuite import org.scalatestplus.mockito.MockitoSugar import sparkz.core._ import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.util.Optional import scala.collection.mutable.{ArrayBuffer, ListBuffer} import scala.io.Source import scala.util.Random @@ -163,6 +167,37 @@ class AccountStateMetadataStorageViewTest ) } + @Test + def doNotRemoveOldCertificateIfAreStillNeededAsPreviousCertificate(): Unit = { + // Arrange + val storageMock = mock[Storage] + val metadataStorageView = new AccountStateMetadataStorageView(storageMock) + val storageCertKey = new ByteArrayWrapper(BytesUtils.fromHexString("fecbe6fcc71e68d667c602c6112bec5e365c77f0fcb829d2502afb4e0608eaba")) + + when(storageMock.get(storageCertKey)).thenAnswer(_ => Optional.empty()) + + // Act + val oldCertToBeRemoved = metadataStorageView.getOldTopCertificatesToBeRemoved(WithdrawalEpochInfo(5, 5)) + + // Assert + assertTrue(oldCertToBeRemoved.isEmpty) + } + + @Test + def removeOldCertificateIfCertificateWasAlreadyUsedAsPreviousCertificate(): Unit = { + // Arrange + val storageMock = mock[Storage] + val metadataStorageView = new AccountStateMetadataStorageView(storageMock) + val storageCertKey = new ByteArrayWrapper(BytesUtils.fromHexString("fecbe6fcc71e68d667c602c6112bec5e365c77f0fcb829d2502afb4e0608eaba")) + + when(storageMock.get(storageCertKey)).thenAnswer(_ => Optional.of(Some(2))) + + // Act + val oldCertToBeRemoved = metadataStorageView.getOldTopCertificatesToBeRemoved(WithdrawalEpochInfo(5, 5)) + + // Assert + assertTrue(oldCertToBeRemoved.nonEmpty) + } def generateCertificateWithEpochNumber(epochNum: Int): WithdrawalEpochCertificate = { val sourceCertHex: String = Source.fromResource("cert_no_bts").getLines().next() diff --git a/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala b/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala index 6f9593918d..65095ff19f 100644 --- a/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala +++ b/sdk/src/test/scala/io/horizen/account/utils/AccountMockDataHelper.scala @@ -452,10 +452,10 @@ case class AccountMockDataHelper(genesis: Boolean) private def setupMockMessageProcessor = { val mockMsgProcessor = mock[MessageProcessor] Mockito - .when(mockMsgProcessor.canProcess(any[Message], any[BaseAccountStateView], any[Int])) + .when(mockMsgProcessor.canProcess(any[Invocation], any[BaseAccountStateView], any[Int])) .thenReturn(true) Mockito - .when(mockMsgProcessor.process(any[Message], any[BaseAccountStateView], any[GasPool], any[BlockContext])) + .when(mockMsgProcessor.process(any[Invocation], any[BaseAccountStateView], any[ExecutionContext])) .thenReturn(Array.empty[Byte]) mockMsgProcessor } diff --git a/sdk/src/test/scala/io/horizen/api/http/route/SidechainApplicationApiRouteTest.scala b/sdk/src/test/scala/io/horizen/api/http/route/SidechainApplicationApiRouteTest.scala index 1deaad79a3..3e435c78c8 100644 --- a/sdk/src/test/scala/io/horizen/api/http/route/SidechainApplicationApiRouteTest.scala +++ b/sdk/src/test/scala/io/horizen/api/http/route/SidechainApplicationApiRouteTest.scala @@ -2,16 +2,20 @@ package io.horizen.api.http.route import akka.http.scaladsl.model.{ContentTypes, StatusCodes} import akka.http.scaladsl.server.Route +import akka.http.scaladsl.testkit.RouteTestTimeout import io.horizen.json.SerializationUtil import io.horizen.utxo.api.http.SimpleCustomApi import org.junit.Assert.{assertEquals, assertTrue} import scala.collection.JavaConverters._ +import scala.concurrent.duration._ class SidechainApplicationApiRouteTest extends SidechainApiRouteTest { override val basePath = "/customSecret/" + implicit val timeout = RouteTestTimeout(2.second) + "The Api should to" should { "reject and reply with http error" in { diff --git a/sdk/src/test/scala/io/horizen/consensus/ConsensusDataProviderTest.scala b/sdk/src/test/scala/io/horizen/consensus/ConsensusDataProviderTest.scala index 0c61d34896..a66a3ff4a8 100644 --- a/sdk/src/test/scala/io/horizen/consensus/ConsensusDataProviderTest.scala +++ b/sdk/src/test/scala/io/horizen/consensus/ConsensusDataProviderTest.scala @@ -23,12 +23,11 @@ import scala.collection.mutable.ListBuffer class TestedConsensusDataProvider(slotsPresentation: List[List[Int]], - val params: NetworkParams, consensusSlotsPerEpoch: Int, consensusSecondsInSlot: Int) + val params: NetworkParams) extends ConsensusDataProvider with NetworkParamsUtils with SparkzLogging { - require(slotsPresentation.forall(_.size == consensusSlotsPerEpoch)) private val dummyWithdrawalEpochInfo = utils.WithdrawalEpochInfo(0, 0) val testVrfProofData: String = "bf4d2892d7562e973ba8a60ef5f9262c088811cc3180c3389b1cef3a66dcfb390d9bb91cebab11bcae871d6a6bd203292264d1002ac70b539f7025a9a813637e1866b2d5c289f28646385549bac7681ef659f2d1d8ca1a21037b036c7925b692e8" @@ -68,7 +67,7 @@ class TestedConsensusDataProvider(slotsPresentation: List[List[Int]], val previousId: ModifierId = acc.last.last._1 val nextTimeStamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(index + 2), intToConsensusSlotNumber(1)) val newData = - generateBlockIdsAndInfosIter(previousId, consensusSecondsInSlot, nextTimeStamp, previousId, ListBuffer[(ModifierId, SidechainBlockInfo)](), processed) + generateBlockIdsAndInfosIter(previousId, nextTimeStamp, previousId, ListBuffer[(ModifierId, SidechainBlockInfo)](), processed) acc.append(newData) acc } @@ -76,18 +75,19 @@ class TestedConsensusDataProvider(slotsPresentation: List[List[Int]], @tailrec final def generateBlockIdsAndInfosIter(previousId: ModifierId, - secondsInSlot: Int, nextTimestamp: Long, lastBlockInPreviousConsensusEpoch: ModifierId, acc: ListBuffer[(ModifierId, SidechainBlockInfo)], vrfData: List[Option[(VrfProof, VrfOutput)]]): Seq[(ModifierId, SidechainBlockInfo)] = { + val epochNumber = TimeToEpochUtils.timeStampToEpochNumber(params.sidechainGenesisBlockTimestamp, nextTimestamp) + val secondsInSlot = ConsensusParamsUtil.getConsensusSecondsInSlotsPerEpoch(epochNumber) vrfData.headOption match { case Some(Some((vrfProof, vrfOutput))) => { val idInfo = generateSidechainBlockInfo(previousId, nextTimestamp, vrfProof, vrfOutput, lastBlockInPreviousConsensusEpoch) acc += idInfo - generateBlockIdsAndInfosIter(idInfo._1, secondsInSlot, nextTimestamp + secondsInSlot, lastBlockInPreviousConsensusEpoch, acc, vrfData.tail) + generateBlockIdsAndInfosIter(idInfo._1, nextTimestamp + secondsInSlot, lastBlockInPreviousConsensusEpoch, acc, vrfData.tail) } - case Some(None) => generateBlockIdsAndInfosIter(previousId, secondsInSlot, nextTimestamp + secondsInSlot, lastBlockInPreviousConsensusEpoch, acc, vrfData.tail) + case Some(None) => generateBlockIdsAndInfosIter(previousId, nextTimestamp + secondsInSlot, lastBlockInPreviousConsensusEpoch, acc, vrfData.tail) case None => acc } } @@ -134,11 +134,54 @@ class ConsensusDataProviderTest extends CompanionsFixture{ val generator: SidechainBlockFixture = new SidechainBlockFixture {} val dummyWithdrawalEpochInfo = utils.WithdrawalEpochInfo(0, 0) val slotsInEpoch = 10 - val secondsInSlot = 100 + val secondsInSlot = 10 + val startFork0 = 0 + + val slotsInEpoch3 = 11 + val secondsInSlot3 = 12 + val startFork3 = 3 + + val slotsInEpoch4 = 12 + val secondsInSlot4 = 14 + val startFork4 = 4 + + val slotsInEpoch5 = 13 + val secondsInSlot5 = 16 + val startFork5 = 5 + + val slotsInEpoch6 = 14 + val secondsInSlot6 = 18 + val startFork6 = 6 + + val slotsInEpoch7 = 15 + val secondsInSlot7 = 20 + val startFork7 = 7 + + val slotsInEpoch8 = 16 + val secondsInSlot8 = 22 + val startFork8 = 8 + + val slotsInEpoch9 = 17 + val secondsInSlot9 = 24 + val startFork9 = 9 + + val slotsInEpoch10 = 18 + val secondsInSlot10 = 26 + val startFork10 = 10 @Before def init(): Unit = { - ForkManagerUtil.initializeForkManager(CustomForkConfiguratorWithConsensusParamsFork.getCustomForkConfiguratorWithConsensusParamsFork(Seq(0), Seq(slotsInEpoch), Seq(secondsInSlot)), "regtest") + ForkManagerUtil.initializeForkManager( + CustomForkConfiguratorWithConsensusParamsFork.getCustomForkConfiguratorWithConsensusParamsFork( + Seq( + startFork0,startFork3,startFork4,startFork5,startFork6,startFork7,startFork8, startFork9, startFork10 + ), + Seq( + slotsInEpoch, slotsInEpoch3,slotsInEpoch4,slotsInEpoch5,slotsInEpoch6,slotsInEpoch7,slotsInEpoch8,slotsInEpoch9, slotsInEpoch10 + ), + Seq( + secondsInSlot, secondsInSlot3,secondsInSlot4,secondsInSlot5,secondsInSlot6,secondsInSlot7,secondsInSlot8,secondsInSlot9, secondsInSlot10 + )), "regtest") } @Test @@ -147,17 +190,17 @@ class ConsensusDataProviderTest extends CompanionsFixture{ // 1 -- block in slot is present; 0 -- no block for slot // slots 1 2 3 4 5 6 7 8 9 10 List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), //2 epoch - List(1, 1, 0, 0, 1, 1, 1, 1, 0, 0), //3 epoch - List(1, 1, 0, 0, 1, 1, 1, 1, 0, 0), //4 epoch - List(0, 0, 0, 0, 1, 1, 1, 1, 0, 0), //5 epoch - List(1, 0, 0, 0, 0, 0, 0, 0, 0, 1), //6 epoch - List(1, 0, 1, 0, 1, 0, 1, 0, 1, 0), //7 epoch - List(1, 0, 1, 0, 1, 0, 1, 0, 1, 0), //8 epoch - List(0, 1, 1, 0, 1, 0, 1, 1, 1, 1), //9 epoch - List(0, 1, 1, 0, 1, 0, 1, 1, 0, 1), //10 epoch + List(1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0), //3 epoch + List(1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1), //4 epoch + List(0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0), //5 epoch + + List(1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0), //6 epoch + List(1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0), //7 epoch + List(0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0), //8 epoch + List(0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0), //9 epoch + List(0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0), //10 epoch ) - require(slotsPresentationForFirstDataProvider.forall(_.size == slotsInEpoch)) val genesisBlockId = bytesToId(Utils.doubleSHA256Hash("genesis".getBytes(StandardCharsets.UTF_8))) val genesisBlockTimestamp = 1000000 @@ -167,10 +210,28 @@ class ConsensusDataProviderTest extends CompanionsFixture{ ) {override val sidechainGenesisBlockParentId: ModifierId = bytesToId(Utils.doubleSHA256Hash("genesisParent".getBytes(StandardCharsets.UTF_8)))} ConsensusParamsUtil.setConsensusParamsForkActivation(Seq( - ConsensusParamsForkInfo(0, new ConsensusParamsFork(slotsInEpoch, secondsInSlot)) + ConsensusParamsForkInfo(startFork0, new ConsensusParamsFork(slotsInEpoch, secondsInSlot)), + ConsensusParamsForkInfo(startFork3, new ConsensusParamsFork(slotsInEpoch3, secondsInSlot3)), + ConsensusParamsForkInfo(startFork4, new ConsensusParamsFork(slotsInEpoch4, secondsInSlot4)), + ConsensusParamsForkInfo(startFork5, new ConsensusParamsFork(slotsInEpoch5, secondsInSlot5)), + ConsensusParamsForkInfo(startFork6, new ConsensusParamsFork(slotsInEpoch6, secondsInSlot6)), + ConsensusParamsForkInfo(startFork7, new ConsensusParamsFork(slotsInEpoch7, secondsInSlot7)), + ConsensusParamsForkInfo(startFork8, new ConsensusParamsFork(slotsInEpoch8, secondsInSlot8)), + ConsensusParamsForkInfo(startFork9, new ConsensusParamsFork(slotsInEpoch9, secondsInSlot9)), + ConsensusParamsForkInfo(startFork10, new ConsensusParamsFork(slotsInEpoch10, secondsInSlot10)) )) - ConsensusParamsUtil.setConsensusParamsForkTimestampActivation(Seq(TimeToEpochUtils.virtualGenesisBlockTimeStamp(networkParams.sidechainGenesisBlockTimestamp))) - val firstDataProvider = new TestedConsensusDataProvider(slotsPresentationForFirstDataProvider, networkParams, slotsInEpoch, secondsInSlot) + ConsensusParamsUtil.setConsensusParamsForkTimestampActivation(Seq( + TimeToEpochUtils.virtualGenesisBlockTimeStamp(networkParams.sidechainGenesisBlockTimestamp), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork3), intToConsensusSlotNumber(slotsInEpoch3)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork4), intToConsensusSlotNumber(slotsInEpoch4)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork5), intToConsensusSlotNumber(slotsInEpoch5)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork6), intToConsensusSlotNumber(slotsInEpoch6)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork7), intToConsensusSlotNumber(slotsInEpoch7)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork8), intToConsensusSlotNumber(slotsInEpoch8)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork9), intToConsensusSlotNumber(slotsInEpoch9)), + TimeToEpochUtils.getTimeStampForEpochAndSlot(networkParams.sidechainGenesisBlockTimestamp, intToConsensusEpochNumber(startFork10), intToConsensusSlotNumber(slotsInEpoch10)) + )) + val firstDataProvider = new TestedConsensusDataProvider(slotsPresentationForFirstDataProvider, networkParams) val blockIdAndInfosPerEpochForFirstDataProvider = firstDataProvider.blockIdAndInfosPerEpoch val epochIdsForFirstDataProvider = firstDataProvider.epochIds //Finished preparation @@ -195,19 +256,19 @@ class ConsensusDataProviderTest extends CompanionsFixture{ assertEquals(consensusInfoForEndSecondEpoch.stakeConsensusEpochInfo, consensusInfoForGenesisEpoch.stakeConsensusEpochInfo) //and stake is as expected assertEquals(1000, consensusInfoForEndSecondEpoch.stakeConsensusEpochInfo.totalStake) - //nd stake root hash as expected + //and stake root hash as expected assertTrue(epochIdsForFirstDataProvider.head.getBytes(StandardCharsets.UTF_8).take(merkleTreeHashLen).sameElements(consensusInfoForGenesisEpoch.stakeConsensusEpochInfo.rootHash)) //but nonce is the same as well // Is it acceptable? // assertEquals(consensusInfoForEndSecondEpoch.nonceConsensusEpochInfo, consensusInfoForGenesisEpoch.nonceConsensusEpochInfo) //Stake and root hash is the same for second and third epoch assertEquals(consensusInfoForStartSecondEpoch.stakeConsensusEpochInfo, consensusInfoForEndThirdEpoch.stakeConsensusEpochInfo) - //but nonce is differ + //but nonce is different assertNotEquals(consensusInfoForStartSecondEpoch.nonceConsensusEpochInfo, consensusInfoForEndThirdEpoch.nonceConsensusEpochInfo) - //Stake and root hash is differ starting from fourth epoch - assertNotEquals(consensusInfoForGenesisEpoch.stakeConsensusEpochInfo, consensusInfoForEndFourthEpoch.stakeConsensusEpochInfo) - //and nonce is also differ + //Stake and root hash is the same + assertEquals(consensusInfoForGenesisEpoch.stakeConsensusEpochInfo, consensusInfoForEndFourthEpoch.stakeConsensusEpochInfo) + //but nonce is different assertNotEquals(consensusInfoForGenesisEpoch.nonceConsensusEpochInfo, consensusInfoForEndFourthEpoch.nonceConsensusEpochInfo) // regression test @@ -238,25 +299,27 @@ class ConsensusDataProviderTest extends CompanionsFixture{ slotsPresentationForFirstDataProvider(1), //3 epoch slotsPresentationForFirstDataProvider(2), //4 epoch slotsPresentationForFirstDataProvider(3), //5 epoch - List(0, 1, 1, 0, 0, 0, 0, 1, 1, 0), //6 epoch, changed quiet slots compared to original - List(0, 0, 1, 0, 1, 0, 1, 0, 1, 1), //7 epoch, changed quiet slots - List(1, 0, 1, 0, 1, 1, 1, 0, 1, 0), //8 epoch, changed non-quiet slot + + List(0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0), //6 epoch, changed quiet slots compared to original + List(0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0), //7 epoch, changed quiet slots + List(0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0), //8 epoch, changed non-quiet slot + slotsPresentationForFirstDataProvider(7), //9 epoch slotsPresentationForFirstDataProvider(8) //10 epoch ) - val secondDataProider = new TestedConsensusDataProvider(slotsPresentationForSecondDataProvider, networkParams, slotsInEpoch, secondsInSlot) - val blockIdAndInfosPerEpochForSecondDataProvider = secondDataProider.blockIdAndInfosPerEpoch - val epochIdsForSecondDataProvider = secondDataProider.epochIds + val secondDataProvider = new TestedConsensusDataProvider(slotsPresentationForSecondDataProvider, networkParams) + val blockIdAndInfosPerEpochForSecondDataProvider = secondDataProvider.blockIdAndInfosPerEpoch + val epochIdsForSecondDataProvider = secondDataProvider.epochIds //consensus info shall be calculated the same if all previous infos the same val consensusInfoForEndFifthEpoch = firstDataProvider.getInfoForCheckingBlockInEpochNumber(5) - val consensusInfoForEndFifthEpoch2 = secondDataProider.getInfoForCheckingBlockInEpochNumber(5) + val consensusInfoForEndFifthEpoch2 = secondDataProvider.getInfoForCheckingBlockInEpochNumber(5) assertEquals(consensusInfoForEndFifthEpoch, consensusInfoForEndFifthEpoch2) //consensus nonce shall be the same in case if changed quiet slots only val consensusInfoForEndSeventhEpoch = firstDataProvider.getInfoForCheckingBlockInEpochNumber(7) - val consensusInfoForEndSeventhEpoch2 = secondDataProider.getInfoForCheckingBlockInEpochNumber(7) + val consensusInfoForEndSeventhEpoch2 = secondDataProvider.getInfoForCheckingBlockInEpochNumber(7) assertEquals(consensusInfoForEndSeventhEpoch.nonceConsensusEpochInfo, consensusInfoForEndSeventhEpoch2.nonceConsensusEpochInfo) //Stack root shall not be changed as well due it calculated for epoch 5 assertEquals(consensusInfoForEndSeventhEpoch.stakeConsensusEpochInfo.rootHash.deep, consensusInfoForEndSeventhEpoch2.stakeConsensusEpochInfo.rootHash.deep) @@ -264,23 +327,23 @@ class ConsensusDataProviderTest extends CompanionsFixture{ //consensus nonce shall be the same in case if changed quiet slots only val consensusInfoForEndEightEpoch = firstDataProvider.getInfoForCheckingBlockInEpochNumber(8) - val consensusInfoForEndEightEpoch2 = secondDataProider.getInfoForCheckingBlockInEpochNumber(8) + val consensusInfoForEndEightEpoch2 = secondDataProvider.getInfoForCheckingBlockInEpochNumber(8) assertEquals(consensusInfoForEndEightEpoch.nonceConsensusEpochInfo, consensusInfoForEndEightEpoch2.nonceConsensusEpochInfo) //but stack root shall be changed (root hash calculated based on last block id in epoch, last block for epoch 6 in tested1 and in tested2 is differ) assertNotEquals(consensusInfoForEndEightEpoch.stakeConsensusEpochInfo.rootHash.deep, consensusInfoForEndEightEpoch2.stakeConsensusEpochInfo.rootHash.deep) - //consensus nonce shall be the changed due non-quet slots are changed + //consensus nonce shall be the changed due non-quiet slots are changed val consensusInfoForEndNineEpoch = firstDataProvider.getInfoForCheckingBlockInEpochNumber(9) - val consensusInfoForEndNineEpoch2 = secondDataProider.getInfoForCheckingBlockInEpochNumber(9) + val consensusInfoForEndNineEpoch2 = secondDataProvider.getInfoForCheckingBlockInEpochNumber(9) assertNotEquals(consensusInfoForEndNineEpoch.nonceConsensusEpochInfo, consensusInfoForEndNineEpoch2.nonceConsensusEpochInfo) - //consensus nonce shall be the changed due non-quet slots are changed in some previous epoch + //consensus nonce shall be the changed due non-quiet slots are changed in some previous epoch val consensusInfoForEndTenEpoch = firstDataProvider.getInfoForCheckingBlockInEpochNumber(10) - val consensusInfoForEndTenEpoch2 = secondDataProider.getInfoForCheckingBlockInEpochNumber(10) + val consensusInfoForEndTenEpoch2 = secondDataProvider.getInfoForCheckingBlockInEpochNumber(10) assertNotEquals(consensusInfoForEndTenEpoch.nonceConsensusEpochInfo, consensusInfoForEndTenEpoch2.nonceConsensusEpochInfo) } diff --git a/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala b/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala index 0c9ed88ed3..753e512f89 100644 --- a/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala +++ b/sdk/src/test/scala/io/horizen/forger/ForgerGenerationRateTest.scala @@ -59,7 +59,7 @@ class ForgerGenerationRateTest extends JUnitSuite { }) val slotFilledPercentage: Double = (stakes.count(s => s).toDouble / slotNumber) * 100 - assertTrue("Expected stakes result", slotFilledPercentage <= 6 && slotFilledPercentage >= 4) + assertTrue("Unexpected slot filled percentage ("+slotFilledPercentage+" is not between 4 and 6)", slotFilledPercentage <= 6 && slotFilledPercentage >= 4) } @Ignore diff --git a/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorOmmersTest.scala b/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorOmmersTest.scala index 09471bb4ec..b06866e72f 100644 --- a/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorOmmersTest.scala +++ b/sdk/src/test/scala/io/horizen/history/validation/ConsensusValidatorOmmersTest.scala @@ -245,7 +245,7 @@ class ConsensusValidatorOmmersTest // Mock history - var slotsInEpoch: Int = 6 + val slotsInEpoch: Int = 6 var history = mockHistory(slotsInEpoch) val previousEpochNumber: ConsensusEpochNumber = ConsensusEpochNumber @@ 2 @@ -319,7 +319,6 @@ class ConsensusValidatorOmmersTest Ommers 2/3;2/4 is in `active` slots for nonce calculation, so for block 3/6 and ommer 3/5 nonce will be different. */ - slotsInEpoch = 6 history = mockHistory(slotsInEpoch) val anotherOmmers: Seq[Ommer[SidechainBlockHeader]] = Seq( @@ -372,6 +371,168 @@ class ConsensusValidatorOmmersTest } } + @Test + def switchingEpochOmmersValidationWithConsensusParamsForkActivation(): Unit = { + // Mock Consensus epoch info data + val postForkEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(postForkEpochNonceBytes) + val postForkFullConsensusEpochInfo = FullConsensusEpochInfo(mock[StakeConsensusEpochInfo], + NonceConsensusEpochInfo(byteArrayToConsensusNonce(postForkEpochNonceBytes))) + + val preForkEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(preForkEpochNonceBytes) + val preForkFullConsensusEpochInfo = FullConsensusEpochInfo(mock[StakeConsensusEpochInfo], + NonceConsensusEpochInfo(byteArrayToConsensusNonce(preForkEpochNonceBytes))) + + val switchedOmmersPostForkEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(switchedOmmersPostForkEpochNonceBytes) + val switchedOmmersFullConsensusEpochInfo = FullConsensusEpochInfo(postForkFullConsensusEpochInfo.stakeConsensusEpochInfo, + NonceConsensusEpochInfo(byteArrayToConsensusNonce(switchedOmmersPostForkEpochNonceBytes))) + + + /* Test 1: Valid Ommers in correct order from the previous and current epoch as VerifiedBlock + Notation / + Slots in epoch: 6 + Block slots number: 2/2 - 3/6 + | + Ommers slots: [2/5 , 3/1 , 3/5] + Ommer 2/5 is in `quite` slots, so for 3/6 block and 3/1 ommer nonce will be the same. + */ + + + // Mock history + val preForkEpochNumber: ConsensusEpochNumber = ConsensusEpochNumber @@ 2 + val postForkEpochNumber: ConsensusEpochNumber = ConsensusEpochNumber @@ 3 + + val slotsInEpoch: Int = 6 + val slotsInEpochAfterFork: Int = 50 + val consensusParamsForkSeq = Seq( + ConsensusParamsForkInfo(0, new ConsensusParamsFork(slotsInEpoch)), + ConsensusParamsForkInfo(postForkEpochNumber, new ConsensusParamsFork(slotsInEpochAfterFork)) + ) + + var history = mockHistoryWithConsensusParameterForks(consensusParamsForkSeq) + + // Mock ommers + val ommers: Seq[Ommer[SidechainBlockHeader]] = Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 5)), // quite slot - no impact on nonce calculation + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 1)), + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 5)) + ) + + // Set initialNonceData (reverse order expected) + val expectedInitialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = Seq( + (generateDummyVrfOutput(ommers.head.header), ConsensusSlotNumber @@ 5) + ) + + // Mock block with ommers + val parentId: ModifierId = getRandomBlockId() + val parentInfo: SidechainBlockInfo = mock[SidechainBlockInfo] + val verifiedBlockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 6) + val verifiedBlock: SidechainBlock = mock[SidechainBlock] + val header = mock[SidechainBlockHeader] + Mockito.when(header.timestamp).thenReturn(verifiedBlockTimestamp) + Mockito.when(verifiedBlock.header).thenReturn(header) + Mockito.when(verifiedBlock.ommers).thenReturn(ommers) + + + Mockito.when(history.calculateNonceForNonGenesisEpoch( + ArgumentMatchers.any[ModifierId], + ArgumentMatchers.any[SidechainBlockInfo], + ArgumentMatchers.any[Seq[(VrfOutput, ConsensusSlotNumber)]])).thenAnswer(answer => { + val lastBlockIdInEpoch: ModifierId = answer.getArgument(0) + val lastBlockInfoInEpoch: SidechainBlockInfo = answer.getArgument(1) + val initialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = answer.getArgument(2) + + assertEquals("On calculate nonce: lastBlockIdInEpoch is different", parentId, lastBlockIdInEpoch) + assertEquals("On calculate nonce: lastBlockInfoInEpoch is different", parentInfo, lastBlockInfoInEpoch) + assertEquals("On calculate nonce: initialNonceData is different", expectedInitialNonceData, initialNonceData) + + // Return nonce same as current epoch nonce + postForkFullConsensusEpochInfo.nonceConsensusEpochInfo + }) + + var switchedEpochConsensusValidator = new BoxConsensusValidator(timeProvider) { + override private[horizen] def verifyForgingStakeInfo(header: SidechainBlockHeaderBase, stakeConsensusEpochInfo: StakeConsensusEpochInfo, vrfOutput: VrfOutput, percentageForkApplied: Boolean, activeSlotCoefficient: Double): Unit = { + val epochAndSlot = TimeToEpochUtils.timestampToEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, header.timestamp) + epochAndSlot.epochNumber match { + case `preForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", preForkFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case `postForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", postForkFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case epoch => jFail(s"Unknown epoch number: $epoch") + } + assertEquals("Different vrfOutput expected", generateDummyVrfOutput(header), vrfOutput) + } + } + + Try { + switchedEpochConsensusValidator.verifyOmmers(verifiedBlock, postForkFullConsensusEpochInfo, Some(preForkFullConsensusEpochInfo), parentId, parentInfo, history, Seq()) + } match { + case Success(_) => + case Failure(e) => throw e // jFail(s"Block with ommers from both the same and previous epoch expected to be Valid, instead exception: ${e.getMessage}") + } + + + /* Test 2: Valid Ommers in correct order from the previous and current epoch as VerifiedBlock + Notation / + Slots in epoch: 6 + Block slots number: 2/2 - 3/6 + | + Ommers slots: [2/3 , 2/4 , 3/15] + Ommers 2/3;2/4 is in `active` slots for nonce calculation, so for block 3/6 and ommer 3/15 nonce will be different. + */ + + history = mockHistoryWithConsensusParameterForks(consensusParamsForkSeq) + + val anotherOmmers: Seq[Ommer[SidechainBlockHeader]] = Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 3)), // active slot - has impact on nonce calculation + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 4)), // active slot - has impact on nonce calculation + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 15)) + ) + + // Set initialNonceData (reverse order expected) + val anotherExpectedInitialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = Seq( + (generateDummyVrfOutput(anotherOmmers(1).header), ConsensusSlotNumber @@ 4), + (generateDummyVrfOutput(anotherOmmers(0).header), ConsensusSlotNumber @@ 3) + ) + + Mockito.when(verifiedBlock.ommers).thenReturn(anotherOmmers) + + Mockito.when(history.calculateNonceForNonGenesisEpoch( + ArgumentMatchers.any[ModifierId], + ArgumentMatchers.any[SidechainBlockInfo], + ArgumentMatchers.any[Seq[(VrfOutput, ConsensusSlotNumber)]])).thenAnswer(answer => { + val lastBlockIdInEpoch: ModifierId = answer.getArgument(0) + val lastBlockInfoInEpoch: SidechainBlockInfo = answer.getArgument(1) + val initialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = answer.getArgument(2) + + assertEquals("On calculate nonce: lastBlockIdInEpoch is different", parentId, lastBlockIdInEpoch) + assertEquals("On calculate nonce: lastBlockInfoInEpoch is different", parentInfo, lastBlockInfoInEpoch) + assertEquals("On calculate nonce: initialNonceData is different", anotherExpectedInitialNonceData, initialNonceData) + + // Return nonce same different from current epoch nonce + switchedOmmersFullConsensusEpochInfo.nonceConsensusEpochInfo + }) + + switchedEpochConsensusValidator = new BoxConsensusValidator(timeProvider) { + override private[horizen] def verifyForgingStakeInfo(header: SidechainBlockHeaderBase, stakeConsensusEpochInfo: StakeConsensusEpochInfo, vrfOutput: VrfOutput, percentageForkApplied: Boolean, activeSlotCoefficient: Double): Unit = { + val epochAndSlot = TimeToEpochUtils.timestampToEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, header.timestamp) + epochAndSlot.epochNumber match { + case `preForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", preForkFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case `postForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", switchedOmmersFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case epoch => jFail(s"Unknown epoch number: $epoch") + } + assertEquals("Different vrfOutput expected", generateDummyVrfOutput(header), vrfOutput) + } + } + + Try { + switchedEpochConsensusValidator.verifyOmmers(verifiedBlock, postForkFullConsensusEpochInfo, Some(preForkFullConsensusEpochInfo), parentId, parentInfo, history, Seq()) + } match { + case Success(_) => + case Failure(e) => throw e // jFail(s"Block with ommers from both the same and previous epoch expected to be Valid, instead exception: ${e.getMessage}") + } + } + @Test def switchingEpochOmmersWithSubOmmersValidation(): Unit = { // Mock Consensus epoch info data @@ -488,6 +649,129 @@ class ConsensusValidatorOmmersTest } + @Test + def switchingEpochOmmersWithSubOmmersValidationAfterConsensusParamsFork(): Unit = { + // Mock Consensus epoch info data + val postForkEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(postForkEpochNonceBytes) + val postForkFullConsensusEpochInfo = FullConsensusEpochInfo(mock[StakeConsensusEpochInfo], + NonceConsensusEpochInfo(byteArrayToConsensusNonce(postForkEpochNonceBytes))) + + val preForkEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(preForkEpochNonceBytes) + val preForkFullConsensusEpochInfo = FullConsensusEpochInfo(mock[StakeConsensusEpochInfo], + NonceConsensusEpochInfo(byteArrayToConsensusNonce(preForkEpochNonceBytes))) + + val switchedOmmersCurrentEpochNonceBytes: Array[Byte] = new Array[Byte](8) + scala.util.Random.nextBytes(switchedOmmersCurrentEpochNonceBytes) + val switchedOmmersFullConsensusEpochInfo = FullConsensusEpochInfo(postForkFullConsensusEpochInfo.stakeConsensusEpochInfo, + NonceConsensusEpochInfo(byteArrayToConsensusNonce(switchedOmmersCurrentEpochNonceBytes))) + + + /* Test 1: Valid Ommers with subommers in correct order from the previous and current epoch as VerifiedBlock + Notation / + Slots in epoch: 6 + Block slots number: 2/2 - 3/5 + | + Ommers slots: [2/4 , 2/6 , 3/15] + | | | + Subommers slots:[2/3] [2/5] [3/2 , 3/3] + | + Subommers slots: [3/1] + Ommer 2/3 is in `active` slots for nonce calculation, so for block 3/5 and ommers 3/1; 3/2; 3/3; 3/4 nonce will be different. + */ + + + // Mock history + + val preForkEpochNumber: ConsensusEpochNumber = ConsensusEpochNumber @@ 2 + val postForkEpochNumber: ConsensusEpochNumber = ConsensusEpochNumber @@ 3 + + val slotsInEpoch: Int = 6 + val slotsInEpochAfterFork: Int = 50 + val consensusParamsForkSeq = Seq( + ConsensusParamsForkInfo(0, new ConsensusParamsFork(slotsInEpoch)), + ConsensusParamsForkInfo(postForkEpochNumber, new ConsensusParamsFork(slotsInEpochAfterFork)) + ) + + val history = mockHistoryWithConsensusParameterForks(consensusParamsForkSeq) + + // Mock ommers + val ommers: Seq[Ommer[SidechainBlockHeader]] = Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 4), // active slot - has impact on nonce calculation + Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 3)) + )), + + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 6), // quite slot - no impact on nonce calculation + Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, preForkEpochNumber, ConsensusSlotNumber @@ 5)) + )), + + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 15), + Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 2), + Seq( + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 1)) + )), + getMockedOmmer(TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 3)) + )) + ) + + // Set initialNonceData (reverse order expected) + val expectedInitialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = Seq( + (generateDummyVrfOutput(ommers(1).header), ConsensusSlotNumber @@ 6), + (generateDummyVrfOutput(ommers(0).header), ConsensusSlotNumber @@ 4) + ) + + // Mock block with ommers + val parentId: ModifierId = getRandomBlockId() + val parentInfo: SidechainBlockInfo = mock[SidechainBlockInfo] + val verifiedBlockTimestamp = TimeToEpochUtils.getTimeStampForEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, postForkEpochNumber, ConsensusSlotNumber @@ 5) + val verifiedBlock: SidechainBlock = mock[SidechainBlock] + val header = mock[SidechainBlockHeader] + Mockito.when(header.timestamp).thenReturn(verifiedBlockTimestamp) + Mockito.when(verifiedBlock.header).thenReturn(header) + Mockito.when(verifiedBlock.ommers).thenReturn(ommers) + + + Mockito.when(history.calculateNonceForNonGenesisEpoch( + ArgumentMatchers.any[ModifierId], + ArgumentMatchers.any[SidechainBlockInfo], + ArgumentMatchers.any[Seq[(VrfOutput, ConsensusSlotNumber)]])).thenAnswer(answer => { + val lastBlockIdInEpoch: ModifierId = answer.getArgument(0) + val lastBlockInfoInEpoch: SidechainBlockInfo = answer.getArgument(1) + val initialNonceData: Seq[(VrfOutput, ConsensusSlotNumber)] = answer.getArgument(2) + + assertEquals("On calculate nonce: lastBlockIdInEpoch is different", parentId, lastBlockIdInEpoch) + assertEquals("On calculate nonce: lastBlockInfoInEpoch is different", parentInfo, lastBlockInfoInEpoch) + assertEquals("On calculate nonce: initialNonceData is different", expectedInitialNonceData, initialNonceData) + + // Return nonce same different from current epoch nonce + switchedOmmersFullConsensusEpochInfo.nonceConsensusEpochInfo + }) + + val switchedEpochConsensusValidator = new BoxConsensusValidator(timeProvider) { + override private[horizen] def verifyForgingStakeInfo(header: SidechainBlockHeaderBase, stakeConsensusEpochInfo: StakeConsensusEpochInfo, vrfOutput: VrfOutput, percentageForkApplied: Boolean, activeSlotCoefficient: Double): Unit = { + val epochAndSlot = TimeToEpochUtils.timestampToEpochAndSlot(history.params.sidechainGenesisBlockTimestamp, header.timestamp) + epochAndSlot.epochNumber match { + case `preForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", preForkFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case `postForkEpochNumber` => assertEquals("Different stakeConsensusEpochInfo expected", switchedOmmersFullConsensusEpochInfo.stakeConsensusEpochInfo, stakeConsensusEpochInfo) + case epoch => jFail(s"Unknown epoch number: $epoch") + } + assertEquals("Different vrfOutput expected", generateDummyVrfOutput(header), vrfOutput) + } + } + + Try { + switchedEpochConsensusValidator.verifyOmmers(verifiedBlock, postForkFullConsensusEpochInfo, Some(preForkFullConsensusEpochInfo), parentId, parentInfo, history, Seq()) + } match { + case Success(_) => + case Failure(e) => throw e + } + + } + private def getMockedOmmer(timestamp: Long, subOmmers: Seq[Ommer[SidechainBlockHeader]] = Seq()): Ommer[SidechainBlockHeader] = { val header = mock[SidechainBlockHeader] Mockito.when(header.timestamp).thenReturn(timestamp) @@ -520,4 +804,30 @@ class ConsensusValidatorOmmersTest history } + + private def mockHistoryWithConsensusParameterForks(consensusParamsFork: Seq[ConsensusParamsForkInfo]): SidechainHistory = { + val params: NetworkParams = MainNetParams() + + ConsensusParamsUtil.setConsensusParamsForkActivation(consensusParamsFork) + + consensusParamsFork.foreach(paramsFork => { + ConsensusParamsUtil.setConsensusParamsForkTimestampActivation( + Seq( + TimeToEpochUtils.virtualGenesisBlockTimeStamp(params.sidechainGenesisBlockTimestamp), + TimeToEpochUtils.getTimeStampForEpochAndSlot(params.sidechainGenesisBlockTimestamp, ConsensusEpochNumber @@ paramsFork.activationEpoch, ConsensusSlotNumber @@ 0) + ) + ) + }) + + val history: SidechainHistory = mock[SidechainHistory] + + Mockito.when(history.params).thenReturn(params) + + Mockito.when(history.getVrfOutput(ArgumentMatchers.any[SidechainBlockHeader], ArgumentMatchers.any[NonceConsensusEpochInfo])).thenAnswer(answer => { + val header: SidechainBlockHeader = answer.getArgument(0) + Some(generateDummyVrfOutput(header)) + }) + + history + } } diff --git a/sdk/src/test/scala/io/horizen/utxo/network/SidechainNodeViewSynchronizerTest.scala b/sdk/src/test/scala/io/horizen/utxo/network/SidechainNodeViewSynchronizerTest.scala index b81f1a4d77..1a0a7548b8 100644 --- a/sdk/src/test/scala/io/horizen/utxo/network/SidechainNodeViewSynchronizerTest.scala +++ b/sdk/src/test/scala/io/horizen/utxo/network/SidechainNodeViewSynchronizerTest.scala @@ -17,7 +17,7 @@ import org.mockito.{ArgumentMatchers, Mockito} import org.scalatestplus.junit.JUnitSuite import org.scalatestplus.mockito.MockitoSugar import sparkz.core.NodeViewHolder.ReceivableMessages.{GetNodeViewChanges, ModifiersFromRemote, TransactionsFromRemote} -import sparkz.core.network.ModifiersStatus.Requested +import sparkz.core.network.ModifiersStatus.{Held, Requested} import sparkz.core.network.NetworkController.ReceivableMessages.{PenalizePeer, RegisterMessageSpecs, StartConnectingPeers} import sparkz.core.network.NodeViewSynchronizer.ReceivableMessages.SyntacticallyFailedModification import sparkz.core.network.message.{Message, MessageSerializer, ModifiersData, ModifiersSpec} @@ -175,8 +175,8 @@ class SidechainNodeViewSynchronizerTest extends JUnitSuite val file = new FileReader(classLoader.getResource("sidechainblock_hex").getFile) val blockBytes = BytesUtils.fromHexString(new BufferedReader(file).readLine()) - val additianalBytes: Array[Byte] = Array(0x00, 0x0a, 0x01, 0x0b) - val transferData = blockBytes ++ additianalBytes + val additionalBytes: Array[Byte] = Array(0x00, 0x0a, 0x01, 0x0b) + val transferData = blockBytes ++ additionalBytes val sidechainTransactionsCompanion: SidechainTransactionsCompanion = getDefaultTransactionsCompanion val sidechainBlockSerializer = new SidechainBlockSerializer(sidechainTransactionsCompanion) @@ -187,15 +187,32 @@ class SidechainNodeViewSynchronizerTest extends JUnitSuite Mockito.reset(deliveryTracker) Mockito.when(deliveryTracker.status(ArgumentMatchers.any[ModifierId])).thenAnswer(answer => { + // First status() is checked on block itself, then it is checked on parent block val receivedId: ModifierId = answer.getArgument(0) assertEquals("Different block id expected.", deserializedBlock.id, receivedId) Requested - }) + }).thenAnswer(answer => { + val receivedId: ModifierId = answer.getArgument(0) + assertEquals("Expected parent block id.", deserializedBlock.parentId, receivedId) + Held} + ) nodeViewSynchronizerRef ! roundTrip(Message(modifiersSpec, Right(ModifiersData(SidechainBlockBase.ModifierTypeId, Seq(deserializedBlock.id -> blockBytes))), Some(peer))) viewHolderProbe.expectMsgType[ModifiersFromRemote[SidechainBlock]] networkControllerProbe.expectNoMessage() + Mockito.reset(deliveryTracker) + Mockito.when(deliveryTracker.status(ArgumentMatchers.any[ModifierId])).thenAnswer(answer => { + // First status() is checked on block itself, then it is checked on parent block + val receivedId: ModifierId = answer.getArgument(0) + assertEquals("Different block id expected.", deserializedBlock.id, receivedId) + Requested + }).thenAnswer(answer => { + val receivedId: ModifierId = answer.getArgument(0) + assertEquals("Expected parent block id.", deserializedBlock.parentId, receivedId) + Held + } + ) nodeViewSynchronizerRef ! roundTrip(Message(modifiersSpec, Right(ModifiersData(SidechainBlockBase.ModifierTypeId, Seq(deserializedBlock.id -> transferData))), Some(peer))) viewHolderProbe.expectMsgType[ModifiersFromRemote[SidechainBlock]] // Check that sender was penalize @@ -243,4 +260,4 @@ class SidechainNodeViewSynchronizerTest extends JUnitSuite (nodeViewSynchronizerRef, tracker, block, peer, networkControllerProbe, viewHolderProbe, messageSerializer) } -} +} \ No newline at end of file diff --git a/tools/dbtool/pom.xml b/tools/dbtool/pom.xml index 37eadbd7b7..153ee8ed7a 100644 --- a/tools/dbtool/pom.xml +++ b/tools/dbtool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-dbtools - 0.8.1 + 0.9.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.8.1 + 0.9.0 junit diff --git a/tools/sctool/pom.xml b/tools/sctool/pom.xml index 1862926af2..a6a708220f 100644 --- a/tools/sctool/pom.xml +++ b/tools/sctool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-scbootstrappingtools - 0.8.1 + 0.9.0 ${project.groupId}:${project.artifactId} This module offers a way to create a sidechain's configuration file and some utilities. https://github.com/${project.github.organization}/${project.artifactId} @@ -49,7 +49,7 @@ io.horizen sidechains-sdk - 0.8.1 + 0.9.0 compile diff --git a/tools/sidechains-sdk-account_sctools/pom.xml b/tools/sidechains-sdk-account_sctools/pom.xml index e934941dca..1d1053f36c 100644 --- a/tools/sidechains-sdk-account_sctools/pom.xml +++ b/tools/sidechains-sdk-account_sctools/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-account_sctools - 0.8.1 + 0.9.0 ${project.groupId}:${project.artifactId} This module offers a way to create a sidechain's configuration file and some utilities (account model). https://github.com/${project.github.organization}/${project.artifactId} @@ -48,7 +48,7 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.8.1 + 0.9.0 compile diff --git a/tools/sidechains-sdk-utxo_sctools/pom.xml b/tools/sidechains-sdk-utxo_sctools/pom.xml index 9b0961b8f9..2bbb251a54 100644 --- a/tools/sidechains-sdk-utxo_sctools/pom.xml +++ b/tools/sidechains-sdk-utxo_sctools/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-utxo_sctools - 0.8.1 + 0.9.0 ${project.groupId}:${project.artifactId} This module offers a way to create a sidechain's configuration file and some utilities (utxo model). https://github.com/${project.github.organization}/${project.artifactId} @@ -48,7 +48,7 @@ io.horizen sidechains-sdk-scbootstrappingtools - 0.8.1 + 0.9.0 compile diff --git a/tools/signingtool/pom.xml b/tools/signingtool/pom.xml index d242851610..e62d78cf80 100644 --- a/tools/signingtool/pom.xml +++ b/tools/signingtool/pom.xml @@ -3,7 +3,7 @@ 4.0.0 io.horizen sidechains-sdk-signingtools - 0.8.1 + 0.9.0 2022 UTF-8 @@ -15,7 +15,7 @@ io.horizen sidechains-sdk - 0.8.1 + 0.9.0 junit